toolcraft 0.0.5 → 0.0.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 (149) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +77 -59
  4. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
  5. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
  6. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
  7. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
  8. package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
  9. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
  10. package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
  11. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
  20. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
  21. package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
  24. package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
  25. package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
  27. package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
  28. package/node_modules/@poe-code/agent-defs/package.json +20 -0
  29. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
  30. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
  31. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
  32. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
  33. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
  34. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
  35. package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
  38. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
  42. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
  43. package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
  44. package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
  45. package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
  46. package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
  47. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
  48. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
  49. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
  50. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
  51. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
  52. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
  53. package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
  54. package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
  55. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
  56. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
  57. package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
  58. package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
  59. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
  60. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
  61. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
  62. package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
  63. package/node_modules/@poe-code/config-mutations/package.json +33 -0
  64. package/node_modules/@poe-code/file-lock/README.md +52 -0
  65. package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
  66. package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
  67. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
  68. package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
  69. package/node_modules/@poe-code/file-lock/package.json +23 -0
  70. package/node_modules/auth-store/README.md +47 -0
  71. package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
  72. package/node_modules/auth-store/dist/create-secret-store.js +35 -0
  73. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
  74. package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
  75. package/node_modules/auth-store/dist/index.d.ts +7 -0
  76. package/node_modules/auth-store/dist/index.js +4 -0
  77. package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
  78. package/node_modules/auth-store/dist/keychain-store.js +111 -0
  79. package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
  80. package/node_modules/auth-store/dist/provider-store.js +28 -0
  81. package/node_modules/auth-store/dist/types.d.ts +20 -0
  82. package/node_modules/auth-store/dist/types.js +1 -0
  83. package/node_modules/auth-store/package.json +25 -0
  84. package/node_modules/mcp-oauth/README.md +31 -0
  85. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
  86. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
  87. package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
  88. package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
  89. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
  90. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
  91. package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
  92. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
  93. package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
  94. package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
  95. package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
  96. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
  97. package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
  98. package/node_modules/mcp-oauth/dist/client/types.js +1 -0
  99. package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
  100. package/node_modules/mcp-oauth/dist/index.js +7 -0
  101. package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
  102. package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
  103. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
  104. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
  105. package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
  106. package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
  107. package/node_modules/mcp-oauth/package.json +31 -0
  108. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
  109. package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
  110. package/node_modules/tiny-mcp-client/dist/index.js +1 -0
  111. package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
  112. package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
  113. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
  114. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
  115. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
  116. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
  117. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
  118. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
  119. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
  120. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
  121. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
  122. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
  123. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
  124. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
  125. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
  126. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
  127. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
  128. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
  129. package/node_modules/tiny-mcp-client/package.json +22 -0
  130. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
  131. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
  132. package/node_modules/tiny-mcp-client/src/index.ts +94 -0
  133. package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
  134. package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
  135. package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
  136. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
  137. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
  138. package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
  139. package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
  140. package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
  141. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
  142. package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
  144. package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
  145. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
  146. package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
  147. package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
  148. package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
  149. package/package.json +24 -11
@@ -0,0 +1,491 @@
1
+ import { URL } from "node:url";
2
+ import { createAuthStoreClientStore, createAuthStoreSessionStore, } from "./auth-store-session-store.js";
3
+ import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
4
+ import { createAuthorizationState } from "./authorization-state.js";
5
+ import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
6
+ import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse, } from "./token-endpoint.js";
7
+ import { canonicalizeResourceIndicator } from "../resource-indicator.js";
8
+ export function createOAuthClientProvider(options) {
9
+ if (isProviderOptions(options)) {
10
+ return options.provider;
11
+ }
12
+ return createDefaultOAuthClientProvider(options);
13
+ }
14
+ export function createDefaultOAuthClientProvider(options) {
15
+ const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
16
+ const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
17
+ const now = options.now ?? Date.now;
18
+ const sessions = new Map();
19
+ const registeredClients = new Map();
20
+ const refreshPromises = new Map();
21
+ const authorizationPromises = new Map();
22
+ return {
23
+ async authorizeRequest(input) {
24
+ assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
25
+ const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
26
+ const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false);
27
+ const accessToken = session?.tokens?.accessToken;
28
+ if (session === null || accessToken === undefined) {
29
+ return;
30
+ }
31
+ assertRequestMatchesResource(requestUrl, session.resource);
32
+ input.headers.set("Authorization", `Bearer ${accessToken}`);
33
+ },
34
+ async handleUnauthorized(input) {
35
+ try {
36
+ assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
37
+ const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
38
+ const resource = canonicalizeResourceIndicator(input.discovery.resource);
39
+ assertRequestMatchesResource(requestUrl, resource);
40
+ const forceRefresh = hasCachedAccessToken(await loadSession(resource))
41
+ && input.challenge?.params.error === "invalid_token";
42
+ const session = await ensureAuthorizedSession(resource, {
43
+ ...input.discovery,
44
+ resource,
45
+ }, input.fetch, true, forceRefresh);
46
+ if (session?.tokens?.accessToken === undefined) {
47
+ return { action: "fail" };
48
+ }
49
+ return { action: "retry" };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ action: "fail",
54
+ error: error instanceof Error ? error : new Error(String(error)),
55
+ };
56
+ }
57
+ },
58
+ };
59
+ async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false) {
60
+ const canonicalResource = canonicalizeResourceIndicator(resource);
61
+ let session = await loadSession(canonicalResource);
62
+ const sessionDiscovery = resolveDiscovery(discovery, session);
63
+ if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
64
+ return session;
65
+ }
66
+ if (session?.tokens?.refreshToken !== undefined
67
+ && sessionDiscovery !== undefined
68
+ && (forceRefresh || isExpired(session.tokens, now))) {
69
+ session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch);
70
+ if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
71
+ return session;
72
+ }
73
+ }
74
+ if (!allowInteractive || sessionDiscovery === undefined) {
75
+ return session;
76
+ }
77
+ if (forceRefresh && session?.tokens !== undefined) {
78
+ return session;
79
+ }
80
+ return authorizeSession(canonicalResource, session, sessionDiscovery, fetch);
81
+ }
82
+ async function refreshSession(resource, session, discovery, fetch) {
83
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
84
+ const inFlight = refreshPromises.get(resource);
85
+ if (inFlight !== undefined) {
86
+ return inFlight;
87
+ }
88
+ const promise = (async () => {
89
+ try {
90
+ if (session.tokens?.refreshToken === undefined) {
91
+ return session;
92
+ }
93
+ let refreshAttempted = false;
94
+ let refreshedTokens;
95
+ while (true) {
96
+ try {
97
+ refreshedTokens = await refreshAccessToken({
98
+ tokenEndpoint: discovery.authorizationServerMetadata.token_endpoint,
99
+ clientId: session.client.clientId,
100
+ clientSecret: session.client.clientSecret,
101
+ refreshToken: session.tokens.refreshToken,
102
+ resource,
103
+ fetch,
104
+ now,
105
+ });
106
+ break;
107
+ }
108
+ catch (error) {
109
+ if (error instanceof OAuthError && error.error === "invalid_grant") {
110
+ const clearedSession = clearSessionTokens(session);
111
+ await saveSession(resource, clearedSession);
112
+ return clearedSession;
113
+ }
114
+ if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
115
+ await clearRegisteredClient(discovery.authorizationServer);
116
+ await clearSession(resource);
117
+ return null;
118
+ }
119
+ if (!refreshAttempted && isRetryableOAuthError(error)) {
120
+ refreshAttempted = true;
121
+ continue;
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+ const updatedSession = {
127
+ ...session,
128
+ tokens: refreshedTokens,
129
+ discovery: toStoredDiscovery(discovery),
130
+ };
131
+ await saveSession(resource, updatedSession);
132
+ return updatedSession;
133
+ }
134
+ finally {
135
+ refreshPromises.delete(resource);
136
+ }
137
+ })();
138
+ refreshPromises.set(resource, promise);
139
+ return promise;
140
+ }
141
+ async function authorizeSession(resource, existingSession, discovery, fetch) {
142
+ const inFlight = authorizationPromises.get(resource);
143
+ if (inFlight !== undefined) {
144
+ return inFlight;
145
+ }
146
+ const promise = (async () => {
147
+ assertS256PkceSupport(discovery.authorizationServerMetadata);
148
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
149
+ let currentSession = existingSession;
150
+ let transientRetryAttempted = false;
151
+ let reRegistrationAttempted = false;
152
+ while (true) {
153
+ const loopback = await createLoopbackAuthorizationSession({
154
+ openBrowser: options.browser.openBrowser,
155
+ readLine: options.browser.readLine,
156
+ createServer: options.browser.createServer,
157
+ landingPage: options.browser.landingPage,
158
+ });
159
+ let resolvedClient = null;
160
+ try {
161
+ resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch);
162
+ const sessionWithoutTokens = {
163
+ resource,
164
+ authorizationServer: discovery.authorizationServer,
165
+ client: resolvedClient.client,
166
+ discovery: toStoredDiscovery(discovery),
167
+ };
168
+ await saveSession(resource, sessionWithoutTokens);
169
+ const verifier = generateCodeVerifier();
170
+ const challenge = generateCodeChallenge(verifier);
171
+ const authorizationUrl = buildAuthorizationUrl({
172
+ metadata: discovery.authorizationServerMetadata,
173
+ resource,
174
+ clientId: resolvedClient.client.clientId,
175
+ redirectUri: loopback.redirectUri,
176
+ codeChallenge: challenge,
177
+ clientMetadata: getClientMetadata(options.client),
178
+ });
179
+ const code = await loopback.waitForCode(authorizationUrl);
180
+ const tokens = await exchangeAuthorizationCode({
181
+ tokenEndpoint: discovery.authorizationServerMetadata.token_endpoint,
182
+ clientId: resolvedClient.client.clientId,
183
+ clientSecret: resolvedClient.client.clientSecret,
184
+ code,
185
+ codeVerifier: verifier,
186
+ redirectUri: loopback.redirectUri,
187
+ resource,
188
+ fetch,
189
+ now,
190
+ });
191
+ const session = {
192
+ ...sessionWithoutTokens,
193
+ tokens,
194
+ };
195
+ await saveSession(resource, session);
196
+ return session;
197
+ }
198
+ catch (error) {
199
+ if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
200
+ reRegistrationAttempted = true;
201
+ await clearRegisteredClient(discovery.authorizationServer);
202
+ await clearSession(resource);
203
+ currentSession = null;
204
+ continue;
205
+ }
206
+ if (!transientRetryAttempted && isRetryableOAuthError(error)) {
207
+ transientRetryAttempted = true;
208
+ await clearSession(resource);
209
+ currentSession = null;
210
+ continue;
211
+ }
212
+ throw error;
213
+ }
214
+ finally {
215
+ loopback.close();
216
+ }
217
+ }
218
+ })();
219
+ const finalPromise = promise.finally(() => {
220
+ authorizationPromises.delete(resource);
221
+ });
222
+ authorizationPromises.set(resource, finalPromise);
223
+ return finalPromise;
224
+ }
225
+ async function resolveClient(existingSession, discovery, redirectUri, fetch) {
226
+ if (options.client.mode === "static") {
227
+ return {
228
+ kind: "static",
229
+ fromStoredRegistration: false,
230
+ client: {
231
+ clientId: options.client.clientId,
232
+ clientSecret: options.client.clientSecret,
233
+ },
234
+ };
235
+ }
236
+ const registrationEndpoint = discovery.authorizationServerMetadata.registration_endpoint;
237
+ if (registrationEndpoint === undefined && options.client.clientId !== undefined) {
238
+ return {
239
+ kind: "static",
240
+ fromStoredRegistration: false,
241
+ client: {
242
+ clientId: options.client.clientId,
243
+ clientSecret: options.client.clientSecret,
244
+ },
245
+ };
246
+ }
247
+ const storedClient = await loadRegisteredClient(discovery.authorizationServer);
248
+ if (storedClient !== null) {
249
+ return {
250
+ kind: "dynamic",
251
+ fromStoredRegistration: true,
252
+ client: storedClient,
253
+ };
254
+ }
255
+ if (registrationEndpoint === undefined) {
256
+ if (existingSession !== null && existingSession.client.clientId.length > 0) {
257
+ return {
258
+ kind: "dynamic",
259
+ fromStoredRegistration: true,
260
+ client: existingSession.client,
261
+ };
262
+ }
263
+ throw new Error("Authorization server metadata is missing registration_endpoint");
264
+ }
265
+ if (existingSession !== null && existingSession.client.clientId.length > 0) {
266
+ const isConfiguredStaticFallback = options.client.clientId !== undefined
267
+ && existingSession.client.clientId === options.client.clientId
268
+ && existingSession.client.clientSecret === options.client.clientSecret;
269
+ if (!isConfiguredStaticFallback) {
270
+ await saveRegisteredClient(discovery.authorizationServer, existingSession.client);
271
+ return {
272
+ kind: "dynamic",
273
+ fromStoredRegistration: true,
274
+ client: existingSession.client,
275
+ };
276
+ }
277
+ }
278
+ const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
279
+ const response = await fetch(registrationEndpoint, {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ },
284
+ body: JSON.stringify(registrationBody),
285
+ });
286
+ const payload = await readOAuthJsonObjectResponse(response);
287
+ if (typeof payload.client_id !== "string" ||
288
+ payload.client_id.length === 0) {
289
+ throw new Error("OAuth client registration response missing client_id");
290
+ }
291
+ const registeredClient = {
292
+ clientId: payload.client_id,
293
+ clientSecret: typeof payload.client_secret === "string" && payload.client_secret.length > 0
294
+ ? payload.client_secret
295
+ : undefined,
296
+ };
297
+ await saveRegisteredClient(discovery.authorizationServer, registeredClient);
298
+ return {
299
+ kind: "dynamic",
300
+ fromStoredRegistration: false,
301
+ client: registeredClient,
302
+ };
303
+ }
304
+ async function loadSession(resource) {
305
+ if (sessions.has(resource)) {
306
+ return sessions.get(resource) ?? null;
307
+ }
308
+ const session = await sessionStore.load(resource);
309
+ sessions.set(resource, session);
310
+ return session;
311
+ }
312
+ async function saveSession(resource, session) {
313
+ sessions.set(resource, session);
314
+ await sessionStore.save(resource, session);
315
+ }
316
+ async function clearSession(resource) {
317
+ sessions.delete(resource);
318
+ await sessionStore.clear(resource);
319
+ }
320
+ async function loadRegisteredClient(issuer) {
321
+ if (registeredClients.has(issuer)) {
322
+ return registeredClients.get(issuer) ?? null;
323
+ }
324
+ if (clientStore === null) {
325
+ return null;
326
+ }
327
+ const client = await clientStore.load(issuer);
328
+ registeredClients.set(issuer, client);
329
+ return client;
330
+ }
331
+ async function saveRegisteredClient(issuer, client) {
332
+ registeredClients.set(issuer, client);
333
+ if (clientStore !== null) {
334
+ await clientStore.save(issuer, client);
335
+ }
336
+ }
337
+ async function clearRegisteredClient(issuer) {
338
+ registeredClients.delete(issuer);
339
+ if (clientStore !== null) {
340
+ await clientStore.clear(issuer);
341
+ }
342
+ }
343
+ }
344
+ function isProviderOptions(options) {
345
+ return "provider" in options;
346
+ }
347
+ function isExpired(tokens, now) {
348
+ return tokens.expiresAt !== null && tokens.expiresAt <= now();
349
+ }
350
+ function resolveDiscovery(discovery, session) {
351
+ if (discovery !== undefined) {
352
+ return {
353
+ ...discovery,
354
+ resource: canonicalizeResourceIndicator(discovery.resource),
355
+ };
356
+ }
357
+ if (session === null) {
358
+ return undefined;
359
+ }
360
+ const metadata = session.discovery.authorizationServerMetadata;
361
+ if (typeof metadata.issuer !== "string" ||
362
+ typeof metadata.authorization_endpoint !== "string" ||
363
+ typeof metadata.token_endpoint !== "string" ||
364
+ !Array.isArray(metadata.code_challenge_methods_supported) ||
365
+ !metadata.code_challenge_methods_supported.includes("S256")) {
366
+ return undefined;
367
+ }
368
+ return {
369
+ resource: canonicalizeResourceIndicator(session.resource),
370
+ resourceMetadataUrl: session.discovery.resourceMetadataUrl,
371
+ resourceMetadata: session.discovery.resourceMetadata,
372
+ authorizationServer: session.authorizationServer,
373
+ authorizationServerMetadataUrl: "",
374
+ authorizationServerMetadata: metadata,
375
+ };
376
+ }
377
+ function clearSessionTokens(session) {
378
+ const nextSession = { ...session };
379
+ delete nextSession.tokens;
380
+ return nextSession;
381
+ }
382
+ function hasCachedAccessToken(session) {
383
+ return session?.tokens?.accessToken !== undefined;
384
+ }
385
+ function getClientMetadata(client) {
386
+ return client.metadata;
387
+ }
388
+ function buildAuthorizationUrl(input) {
389
+ const url = new URL(input.metadata.authorization_endpoint);
390
+ const resource = canonicalizeResourceIndicator(input.resource);
391
+ const state = createAuthorizationState({
392
+ issuer: input.metadata.issuer,
393
+ requireIssuer: input.metadata.authorization_response_iss_parameter_supported === true,
394
+ });
395
+ url.searchParams.set("response_type", "code");
396
+ url.searchParams.set("client_id", input.clientId);
397
+ url.searchParams.set("redirect_uri", input.redirectUri);
398
+ url.searchParams.set("code_challenge", input.codeChallenge);
399
+ url.searchParams.set("code_challenge_method", "S256");
400
+ url.searchParams.set("resource", resource);
401
+ url.searchParams.set("state", state);
402
+ if (input.clientMetadata?.scope !== undefined && input.clientMetadata.scope.length > 0) {
403
+ url.searchParams.set("scope", input.clientMetadata.scope);
404
+ }
405
+ return url.toString();
406
+ }
407
+ function assertS256PkceSupport(metadata) {
408
+ if (!metadata.code_challenge_methods_supported.includes("S256")) {
409
+ throw new Error("Authorization server metadata must advertise code_challenge_methods_supported including S256");
410
+ }
411
+ }
412
+ function normalizeHostname(hostname) {
413
+ return hostname.endsWith(".") ? hostname.slice(0, -1).toLowerCase() : hostname.toLowerCase();
414
+ }
415
+ function isLoopbackHostname(hostname) {
416
+ const normalizedHostname = normalizeHostname(hostname);
417
+ return normalizedHostname === "localhost"
418
+ || normalizedHostname === "::1"
419
+ || normalizedHostname.startsWith("127.");
420
+ }
421
+ function assertSecureUrl(value, label) {
422
+ const url = new URL(value);
423
+ if (url.protocol === "https:") {
424
+ return;
425
+ }
426
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
427
+ return;
428
+ }
429
+ throw new Error(`${label} must use https unless it targets a loopback host`);
430
+ }
431
+ function assertSecureOAuthFlowEndpoints(metadata) {
432
+ assertNoAccessTokenInUrl(metadata.authorization_endpoint, "Authorization endpoint");
433
+ assertNoAccessTokenInUrl(metadata.token_endpoint, "Token endpoint");
434
+ assertSecureUrl(metadata.authorization_endpoint, "Authorization endpoint");
435
+ assertSecureUrl(metadata.token_endpoint, "Token endpoint");
436
+ if (metadata.registration_endpoint !== undefined) {
437
+ assertNoAccessTokenInUrl(metadata.registration_endpoint, "Registration endpoint");
438
+ assertSecureUrl(metadata.registration_endpoint, "Registration endpoint");
439
+ }
440
+ }
441
+ function assertNoAccessTokenInUrl(value, label) {
442
+ const url = value instanceof URL ? new URL(value.toString()) : new URL(value);
443
+ if (url.searchParams.has("access_token")) {
444
+ throw new Error(`${label} must not include access_token in the URI`);
445
+ }
446
+ }
447
+ function assertRequestMatchesResource(requestUrl, resource) {
448
+ if (requestUrl !== resource) {
449
+ throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
450
+ }
451
+ }
452
+ function buildClientRegistrationBody(metadata, redirectUri) {
453
+ const body = {
454
+ redirect_uris: [redirectUri],
455
+ grant_types: ["authorization_code", "refresh_token"],
456
+ response_types: ["code"],
457
+ token_endpoint_auth_method: "none",
458
+ };
459
+ if (metadata?.clientName !== undefined && metadata.clientName.length > 0) {
460
+ body.client_name = metadata.clientName;
461
+ }
462
+ if (metadata?.scope !== undefined && metadata.scope.length > 0) {
463
+ body.scope = metadata.scope;
464
+ }
465
+ if (metadata?.softwareId !== undefined && metadata.softwareId.length > 0) {
466
+ body.software_id = metadata.softwareId;
467
+ }
468
+ if (metadata?.softwareVersion !== undefined && metadata.softwareVersion.length > 0) {
469
+ body.software_version = metadata.softwareVersion;
470
+ }
471
+ return body;
472
+ }
473
+ function toStoredDiscovery(discovery) {
474
+ return {
475
+ resourceMetadataUrl: discovery.resourceMetadataUrl,
476
+ resourceMetadata: discovery.resourceMetadata,
477
+ authorizationServerMetadata: discovery.authorizationServerMetadata,
478
+ };
479
+ }
480
+ function shouldReRegisterStoredDynamicClient(error, client, alreadyAttempted) {
481
+ if (!(error instanceof OAuthError) || error.error !== "invalid_client" || alreadyAttempted) {
482
+ return false;
483
+ }
484
+ if (client === null) {
485
+ return false;
486
+ }
487
+ if ("kind" in client) {
488
+ return client.kind === "dynamic" && client.fromStoredRegistration;
489
+ }
490
+ return true;
491
+ }
@@ -0,0 +1,20 @@
1
+ import http from "node:http";
2
+ export interface OAuthLandingPage {
3
+ title: string;
4
+ body: string;
5
+ }
6
+ export interface LoopbackAuthorizationOptions {
7
+ openBrowser?: (url: string) => Promise<void>;
8
+ readLine?: () => Promise<string>;
9
+ createServer?: () => http.Server;
10
+ landingPage?: OAuthLandingPage;
11
+ callbackPath?: string;
12
+ }
13
+ export interface LoopbackAuthorizationSession {
14
+ redirectUri: string;
15
+ waitForCode(authorizationUrl: string): Promise<string>;
16
+ close(): void;
17
+ }
18
+ export declare function createLoopbackAuthorizationSession(options?: LoopbackAuthorizationOptions): Promise<LoopbackAuthorizationSession>;
19
+ export declare function extractCodeFromInput(input: string): string | null;
20
+ export declare function buildSuccessPage(landingPage?: OAuthLandingPage): string;
@@ -0,0 +1,169 @@
1
+ import http from "node:http";
2
+ import { parseAuthorizationState } from "./authorization-state.js";
3
+ export async function createLoopbackAuthorizationSession(options = {}) {
4
+ const callbackPath = options.callbackPath ?? "/callback";
5
+ const server = options.createServer ? options.createServer() : http.createServer();
6
+ const port = await startServer(server);
7
+ const redirectUri = `http://127.0.0.1:${port}${callbackPath}`;
8
+ return {
9
+ redirectUri,
10
+ async waitForCode(authorizationUrl) {
11
+ return waitForAuthorizationCode(server, authorizationUrl, options, callbackPath);
12
+ },
13
+ close() {
14
+ server.closeAllConnections?.();
15
+ server.close();
16
+ },
17
+ };
18
+ }
19
+ async function startServer(server) {
20
+ return new Promise((resolve) => {
21
+ server.listen(0, "127.0.0.1", () => {
22
+ const address = server.address();
23
+ resolve(address.port);
24
+ });
25
+ });
26
+ }
27
+ function waitForAuthorizationCode(server, authorizationUrl, options, callbackPath) {
28
+ const expectedAuthorization = readExpectedAuthorizationCallback(authorizationUrl);
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ const settle = (fn) => {
32
+ if (!settled) {
33
+ settled = true;
34
+ fn();
35
+ }
36
+ };
37
+ server.on("request", (req, res) => {
38
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
39
+ if (url.pathname !== callbackPath) {
40
+ res.writeHead(404);
41
+ res.end("Not found");
42
+ return;
43
+ }
44
+ const error = url.searchParams.get("error");
45
+ if (error !== null) {
46
+ const description = url.searchParams.get("error_description") ?? error;
47
+ res.writeHead(400);
48
+ res.end(`Authorization failed: ${description}`);
49
+ settle(() => reject(new Error(`OAuth authorization failed: ${error} — ${description}`)));
50
+ return;
51
+ }
52
+ try {
53
+ const code = validateAuthorizationCallbackParameters({
54
+ code: url.searchParams.get("code"),
55
+ state: url.searchParams.get("state"),
56
+ iss: url.searchParams.get("iss"),
57
+ }, expectedAuthorization);
58
+ res.writeHead(200, { "Content-Type": "text/html" });
59
+ res.end(buildSuccessPage(options.landingPage));
60
+ settle(() => resolve(code));
61
+ }
62
+ catch (error) {
63
+ res.writeHead(400);
64
+ res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
65
+ settle(() => reject(error instanceof Error ? error : new Error(String(error))));
66
+ }
67
+ });
68
+ if (options.readLine !== undefined) {
69
+ options.readLine().then((input) => {
70
+ const callbackParameters = extractCallbackParametersFromInput(input);
71
+ if (callbackParameters === null) {
72
+ settle(() => reject(new Error("OAuth callback missing authorization code")));
73
+ return;
74
+ }
75
+ try {
76
+ const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
77
+ settle(() => resolve(code));
78
+ }
79
+ catch (error) {
80
+ settle(() => reject(error instanceof Error ? error : new Error(String(error))));
81
+ }
82
+ }).catch(() => undefined);
83
+ }
84
+ if (options.openBrowser !== undefined) {
85
+ options.openBrowser(authorizationUrl).catch((error) => {
86
+ settle(() => reject(error));
87
+ });
88
+ }
89
+ });
90
+ }
91
+ export function extractCodeFromInput(input) {
92
+ return extractCallbackParametersFromInput(input)?.code ?? null;
93
+ }
94
+ function extractCallbackParametersFromInput(input) {
95
+ const trimmed = input.replaceAll("\r", "").replaceAll("\n", "").trim();
96
+ if (trimmed.length === 0) {
97
+ return null;
98
+ }
99
+ try {
100
+ const url = new URL(trimmed);
101
+ return {
102
+ code: url.searchParams.get("code"),
103
+ state: url.searchParams.get("state"),
104
+ iss: url.searchParams.get("iss"),
105
+ };
106
+ }
107
+ catch {
108
+ return {
109
+ code: trimmed,
110
+ state: null,
111
+ iss: null,
112
+ };
113
+ }
114
+ }
115
+ function readExpectedAuthorizationCallback(authorizationUrl) {
116
+ const url = new URL(authorizationUrl);
117
+ const state = url.searchParams.get("state");
118
+ const parsedState = parseAuthorizationState(state);
119
+ return {
120
+ state,
121
+ issuer: parsedState?.issuer ?? null,
122
+ requireIssuer: parsedState?.requireIssuer ?? false,
123
+ };
124
+ }
125
+ function validateAuthorizationCallbackParameters(callback, expected) {
126
+ if (callback.code === null || callback.code.length === 0) {
127
+ throw new Error("OAuth callback missing authorization code");
128
+ }
129
+ if (expected.state !== null) {
130
+ if (callback.state === null || callback.state.length === 0) {
131
+ throw new Error("OAuth callback missing state");
132
+ }
133
+ if (callback.state !== expected.state) {
134
+ throw new Error("OAuth callback state mismatch");
135
+ }
136
+ }
137
+ if (expected.requireIssuer) {
138
+ if (callback.iss === null || callback.iss.length === 0) {
139
+ throw new Error("OAuth callback missing issuer");
140
+ }
141
+ }
142
+ if (callback.iss !== null
143
+ && callback.iss.length > 0
144
+ && expected.issuer !== null
145
+ && callback.iss !== expected.issuer) {
146
+ throw new Error("OAuth callback issuer mismatch");
147
+ }
148
+ return callback.code;
149
+ }
150
+ function escapeHtml(text) {
151
+ return text
152
+ .replaceAll("&", "&amp;")
153
+ .replaceAll("<", "&lt;")
154
+ .replaceAll(">", "&gt;")
155
+ .replaceAll("\"", "&quot;");
156
+ }
157
+ export function buildSuccessPage(landingPage) {
158
+ const title = landingPage?.title ?? "Connected";
159
+ const body = landingPage?.body ?? "You can close this tab and return to your terminal.";
160
+ return [
161
+ "<!DOCTYPE html>",
162
+ `<html><head><meta charset=utf-8><title>${escapeHtml(title)}</title></head>`,
163
+ "<body style=\"font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0\">",
164
+ "<div style=\"text-align:center\">",
165
+ `<h1>${escapeHtml(title)}</h1>`,
166
+ `<p style="color:#666">${escapeHtml(body)}</p>`,
167
+ "</div></body></html>",
168
+ ].join("");
169
+ }
@@ -0,0 +1,2 @@
1
+ export declare function generateCodeVerifier(): string;
2
+ export declare function generateCodeChallenge(verifier: string): string;
@@ -0,0 +1,7 @@
1
+ import crypto from "node:crypto";
2
+ export function generateCodeVerifier() {
3
+ return crypto.randomBytes(32).toString("base64url");
4
+ }
5
+ export function generateCodeChallenge(verifier) {
6
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
7
+ }