toolcraft 0.0.308 → 0.0.309

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.
package/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.23",
101
+ "version": "0.1.24",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.308",
116
+ "version": "0.0.309",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.308",
126
+ "version": "0.0.309",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.23",
101
+ "version": "0.1.24",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.308",
116
+ "version": "0.0.309",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.308",
126
+ "version": "0.0.309",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.23",
21
+ "version": "0.1.24",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -41,6 +41,7 @@ const verifier = createJwksTokenVerifier({
41
41
  - `mode: "dynamic"` with optional `metadata`
42
42
  - `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
43
43
  - `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
44
+ - `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
44
45
  - `browser.openBrowser(url)` optional
45
46
  - `browser.readLine()` optional
46
47
  - `browser.createServer()` optional
@@ -69,7 +70,10 @@ const verifier = createJwksTokenVerifier({
69
70
  Fixed redirects support `localhost`, `127.0.0.1`, and `::1` over HTTP. Their
70
71
  exact spelling, port, path and query are preserved through registration,
71
72
  authorization and code exchange. Credentials, fragments, port zero and reserved
72
- OAuth callback query parameters are rejected before binding a listener. Omit
73
+ OAuth callback query parameters are rejected before binding a listener.
74
+ `createDefaultOAuthClientProvider` also checks the configured redirect before
75
+ creating the provider. Imported tokens that cannot be sent as HTTP header
76
+ values fail with diagnostics that omit their contents. Omit
73
77
  `redirectUri` to allocate a random loopback port. Standalone callback sessions
74
78
  accept the same `redirectUri`, `signal` and `timeoutMs` options. Cancellation,
75
79
  timeout and explicit close settle pending code waits and release listeners.
@@ -83,6 +87,14 @@ observe the supplied signal and pass it to any work they start.
83
87
  Configure `client.metadata.scope` to request a precise scope set; broader
84
88
  discovery metadata does not override it.
85
89
 
90
+ Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
91
+ `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
92
+ and optional `scope`. A fresh imported token is used only for its resource.
93
+ Discovery binds an expired or explicitly rejected grant before silent refresh,
94
+ using the original configured client. Persisted sessions take precedence,
95
+ including sessions whose tokens have been cleared; an import cannot revive them.
96
+ Input tokens are copied and invalid expiry values fail before authorization.
97
+
86
98
  `createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
87
99
 
88
100
  ## Environment Variables
@@ -2,7 +2,7 @@ import { isIP } from "node:net";
2
2
  import { fetchMcpResponse } from "../http-fetch.js";
3
3
  import { URL } from "node:url";
4
4
  import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
5
- import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
5
+ import { createLoopbackAuthorizationSession, loopbackTarget } from "./loopback-authorization.js";
6
6
  import { createAuthorizationState } from "./authorization-state.js";
7
7
  import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
8
8
  import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
@@ -15,18 +15,51 @@ export function createOAuthClientProvider(options) {
15
15
  return createDefaultOAuthClientProvider(options);
16
16
  }
17
17
  export function createDefaultOAuthClientProvider(options) {
18
+ loopbackTarget(options.browser);
18
19
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
19
20
  const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
20
21
  const now = options.now ?? Date.now;
21
22
  const registeredClients = new Map();
22
23
  const refreshPromises = new Map();
23
24
  const authorizationPromises = new Map();
25
+ if (options.initialGrant !== undefined) {
26
+ let resource;
27
+ try {
28
+ resource = new URL(options.initialGrant.resource);
29
+ }
30
+ catch {
31
+ throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
32
+ }
33
+ if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.hash)
34
+ throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
35
+ }
36
+ const initialGrant = options.initialGrant === undefined ? undefined : {
37
+ resource: canonicalizeResourceIndicator(options.initialGrant.resource),
38
+ tokens: normalizeStoredTokens(options.initialGrant.tokens),
39
+ client: normalizeConfiguredClient(options.client)
40
+ };
41
+ if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
42
+ throw new Error("OAuth initial grant requires valid tokens and the original client ID");
43
+ if (initialGrant?.tokens !== undefined) {
44
+ try {
45
+ new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
46
+ }
47
+ catch {
48
+ throw new Error("OAuth initial grant access token is not a valid HTTP header value");
49
+ }
50
+ }
51
+ let initialGrantConsumed = false;
24
52
  return {
25
53
  async authorizeRequest(input) {
26
54
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
27
55
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
28
56
  const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false, false, input.signal);
29
57
  const accessToken = session?.tokens?.accessToken;
58
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
59
+ initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
60
+ input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
61
+ return;
62
+ }
30
63
  if (session === null ||
31
64
  accessToken === undefined ||
32
65
  session.tokens === undefined ||
@@ -42,7 +75,7 @@ export function createDefaultOAuthClientProvider(options) {
42
75
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
43
76
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
44
77
  assertRequestMatchesResource(requestUrl, resource);
45
- const forceRefresh = hasCachedAccessToken(await loadSession(resource)) &&
78
+ const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || (!initialGrantConsumed && initialGrant?.resource === resource)) &&
46
79
  input.challenge?.params.error === "invalid_token";
47
80
  const session = await ensureAuthorizedSession(resource, {
48
81
  ...input.discovery,
@@ -66,6 +99,8 @@ export function createDefaultOAuthClientProvider(options) {
66
99
  signal?.throwIfAborted();
67
100
  const canonicalResource = canonicalizeResourceIndicator(resource);
68
101
  let session = await loadSession(canonicalResource);
102
+ if (session !== null && initialGrant?.resource === canonicalResource)
103
+ initialGrantConsumed = true;
69
104
  signal?.throwIfAborted();
70
105
  if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
71
106
  throw new Error("OAuth discovery authorization-server issuer mismatch");
@@ -76,6 +111,15 @@ export function createDefaultOAuthClientProvider(options) {
76
111
  await clearSession(canonicalResource);
77
112
  session = null;
78
113
  }
114
+ if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
115
+ initialGrant.tokens !== undefined && initialGrant.client !== null) {
116
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
117
+ session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
118
+ client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
119
+ await saveSession(canonicalResource, session);
120
+ initialGrantConsumed = true;
121
+ signal?.throwIfAborted();
122
+ }
79
123
  const sessionDiscovery = resolveDiscovery(discovery, session);
80
124
  if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
81
125
  return session;
@@ -21,5 +21,10 @@ export interface LoopbackAuthorizationSession {
21
21
  close(): void;
22
22
  }
23
23
  export declare function createLoopbackAuthorizationSession(options?: LoopbackAuthorizationOptions): Promise<LoopbackAuthorizationSession>;
24
+ export declare function loopbackTarget(options: LoopbackAuthorizationOptions): {
25
+ port: number;
26
+ host: string;
27
+ callbackPath: string;
28
+ };
24
29
  export declare function extractCodeFromInput(input: string): string | null;
25
30
  export declare function buildSuccessPage(landingPage?: OAuthLandingPage): string;
@@ -54,7 +54,7 @@ export async function createLoopbackAuthorizationSession(options = {}) {
54
54
  }
55
55
  };
56
56
  }
57
- function loopbackTarget(options) {
57
+ export function loopbackTarget(options) {
58
58
  if (options.redirectUri !== undefined) {
59
59
  let url;
60
60
  try {
@@ -99,6 +99,11 @@ export interface DefaultOAuthClientProviderOptions {
99
99
  };
100
100
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
101
101
  allowInteractive?: boolean;
102
+ /** Import an existing grant for one resource. Persisted sessions take precedence. */
103
+ initialGrant?: {
104
+ resource: string;
105
+ tokens: StoredOAuthTokens;
106
+ };
102
107
  browser: {
103
108
  openBrowser?(url: string): Promise<void>;
104
109
  /** Exact registered HTTP loopback redirect URI. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -176,6 +176,11 @@ interface DefaultOAuthClientProviderOptions {
176
176
  };
177
177
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
178
178
  allowInteractive?: boolean;
179
+ /** Import an existing grant for one resource. Persisted sessions take precedence. */
180
+ initialGrant?: {
181
+ resource: string;
182
+ tokens: StoredOAuthTokens;
183
+ };
179
184
  browser: {
180
185
  openBrowser?(url: string): Promise<void>;
181
186
  /** Exact registered HTTP loopback redirect URI. */
@@ -4594,18 +4594,48 @@ function createOAuthClientProvider(options) {
4594
4594
  return createDefaultOAuthClientProvider(options);
4595
4595
  }
4596
4596
  function createDefaultOAuthClientProvider(options) {
4597
+ loopbackTarget(options.browser);
4597
4598
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
4598
4599
  const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
4599
4600
  const now = options.now ?? Date.now;
4600
4601
  const registeredClients = /* @__PURE__ */ new Map();
4601
4602
  const refreshPromises = /* @__PURE__ */ new Map();
4602
4603
  const authorizationPromises = /* @__PURE__ */ new Map();
4604
+ if (options.initialGrant !== void 0) {
4605
+ let resource;
4606
+ try {
4607
+ resource = new URL2(options.initialGrant.resource);
4608
+ } catch {
4609
+ throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
4610
+ }
4611
+ if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.hash)
4612
+ throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
4613
+ }
4614
+ const initialGrant = options.initialGrant === void 0 ? void 0 : {
4615
+ resource: canonicalizeResourceIndicator(options.initialGrant.resource),
4616
+ tokens: normalizeStoredTokens(options.initialGrant.tokens),
4617
+ client: normalizeConfiguredClient(options.client)
4618
+ };
4619
+ if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
4620
+ throw new Error("OAuth initial grant requires valid tokens and the original client ID");
4621
+ if (initialGrant?.tokens !== void 0) {
4622
+ try {
4623
+ new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
4624
+ } catch {
4625
+ throw new Error("OAuth initial grant access token is not a valid HTTP header value");
4626
+ }
4627
+ }
4628
+ let initialGrantConsumed = false;
4603
4629
  return {
4604
4630
  async authorizeRequest(input) {
4605
4631
  assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
4606
4632
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
4607
4633
  const session = await ensureAuthorizedSession(requestUrl, void 0, input.fetch, false, false, input.signal);
4608
4634
  const accessToken = session?.tokens?.accessToken;
4635
+ if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
4636
+ input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
4637
+ return;
4638
+ }
4609
4639
  if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
4610
4640
  return;
4611
4641
  }
@@ -4618,7 +4648,7 @@ function createDefaultOAuthClientProvider(options) {
4618
4648
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
4619
4649
  const resource = canonicalizeResourceIndicator(input.discovery.resource);
4620
4650
  assertRequestMatchesResource(requestUrl, resource);
4621
- const forceRefresh = hasCachedAccessToken(await loadSession(resource)) && input.challenge?.params.error === "invalid_token";
4651
+ const forceRefresh = (hasCachedAccessToken(await loadSession(resource)) || !initialGrantConsumed && initialGrant?.resource === resource) && input.challenge?.params.error === "invalid_token";
4622
4652
  const session = await ensureAuthorizedSession(resource, {
4623
4653
  ...input.discovery,
4624
4654
  resource
@@ -4640,6 +4670,8 @@ function createDefaultOAuthClientProvider(options) {
4640
4670
  signal?.throwIfAborted();
4641
4671
  const canonicalResource = canonicalizeResourceIndicator(resource);
4642
4672
  let session = await loadSession(canonicalResource);
4673
+ if (session !== null && initialGrant?.resource === canonicalResource)
4674
+ initialGrantConsumed = true;
4643
4675
  signal?.throwIfAborted();
4644
4676
  if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
4645
4677
  throw new Error("OAuth discovery authorization-server issuer mismatch");
@@ -4648,6 +4680,19 @@ function createDefaultOAuthClientProvider(options) {
4648
4680
  await clearSession(canonicalResource);
4649
4681
  session = null;
4650
4682
  }
4683
+ if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
4684
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
4685
+ session = {
4686
+ resource: canonicalResource,
4687
+ authorizationServer: discovery.authorizationServer,
4688
+ client: initialGrant.client,
4689
+ tokens: initialGrant.tokens,
4690
+ discovery: toStoredDiscovery(discovery)
4691
+ };
4692
+ await saveSession(canonicalResource, session);
4693
+ initialGrantConsumed = true;
4694
+ signal?.throwIfAborted();
4695
+ }
4651
4696
  const sessionDiscovery = resolveDiscovery(discovery, session);
4652
4697
  if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4653
4698
  return session;
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.308",
11
+ "version": "0.0.309",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.308",
3
+ "version": "0.0.309",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.308",
3
+ "version": "0.0.309",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.308",
161
+ "toolcraft-schema": "0.0.309",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",