toolcraft 0.0.739 → 0.0.741

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.111",
101
+ "version": "0.1.113",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.739",
116
+ "version": "0.0.741",
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.739",
126
+ "version": "0.0.741",
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.111",
101
+ "version": "0.1.113",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.739",
116
+ "version": "0.0.741",
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.739",
126
+ "version": "0.0.741",
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.111",
21
+ "version": "0.1.113",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -41,6 +41,15 @@ alone do not establish either native error type.
41
41
  - `createJwksTokenVerifier(options)`: JWKS-backed access-token verifier for MCP servers.
42
42
  - `OAuthError`: OAuth HTTP error type with status, retryability and known-outcome fields.
43
43
 
44
+ ## Resumable server authorization
45
+
46
+ `beginRemoteMcpAuthorization` and `completeRemoteMcpAuthorization` are available
47
+ in both Node and Worker exports, and through `@poe-platform/safe-bash/mcp`.
48
+ They use Web Crypto and host storage, with no filesystem, listener, browser opener
49
+ or pending callback promise. Supply a statically registered client and trusted
50
+ discovery. See [the public MCP example](../safe-bash-mcp/README.md#resumable-https-oauth)
51
+ for callback routing and the required atomic storage contract.
52
+
44
53
  ## Configuration
45
54
 
46
55
  `createDefaultOAuthClientProvider(options)` accepts:
@@ -0,0 +1,52 @@
1
+ import type { OAuthDiscoveryResult, OAuthMetadataFetch, StoredOAuthClient, StoredOAuthSession } from "./types.js";
2
+ /** Sensitive, JSON-serializable record. Store only in private, authenticated host storage. */
3
+ export interface RemoteMcpAuthorizationTransaction {
4
+ readonly state: string;
5
+ readonly expiresAt: number;
6
+ readonly redirectUri: string;
7
+ readonly codeVerifier: string;
8
+ readonly requireIssuer: boolean;
9
+ readonly session: StoredOAuthSession;
10
+ }
11
+ /** Bind each store instance to the authenticated user and logical server. */
12
+ export interface RemoteMcpAuthorizationStore {
13
+ /** Atomically insert a unique state and capture the current credential generation. Never overwrite a state. */
14
+ create(transaction: RemoteMcpAuthorizationTransaction): Promise<void>;
15
+ /** Atomically remove/mark consumed, returning at most once across all workers. Preserve the commit fence. */
16
+ consume(state: string): Promise<RemoteMcpAuthorizationTransaction | null>;
17
+ /** Atomically persist only if the consumed transaction's generation is still current.
18
+ * Reset, deletion, reconfiguration and a superseding grant must invalidate the fence.
19
+ * Return false if stale. Never implement as an unconditional credential save.
20
+ */
21
+ commit(transaction: RemoteMcpAuthorizationTransaction, session: StoredOAuthSession): Promise<boolean>;
22
+ }
23
+ export interface BeginRemoteMcpAuthorizationOptions {
24
+ readonly resource: string;
25
+ readonly redirectUri: string;
26
+ readonly client: StoredOAuthClient;
27
+ readonly scope?: string;
28
+ readonly store: RemoteMcpAuthorizationStore;
29
+ /** Host-validated discovery, including issuer trust and network policy. No implicit network access. */
30
+ readonly discover: (resource: string, signal?: AbortSignal) => Promise<OAuthDiscoveryResult>;
31
+ readonly signal?: AbortSignal;
32
+ readonly now?: () => number;
33
+ /** Transaction lifetime, at most ten minutes. */
34
+ readonly ttlMs?: number;
35
+ }
36
+ export interface CompleteRemoteMcpAuthorizationOptions {
37
+ readonly callbackUrl: string;
38
+ readonly store: RemoteMcpAuthorizationStore;
39
+ /** Required host network policy. Redirects are rejected, including at the token endpoint. */
40
+ readonly fetch: OAuthMetadataFetch;
41
+ readonly signal?: AbortSignal;
42
+ readonly now?: () => number;
43
+ }
44
+ /** Start consent without a listener or a pending callback promise. Only the URL leaves private storage. */
45
+ export declare function beginRemoteMcpAuthorization(options: BeginRemoteMcpAuthorizationOptions): Promise<{
46
+ authorizationUrl: string;
47
+ expiresAt: number;
48
+ }>;
49
+ /** Redeem once after any process restart, then conditionally persist. Returns no tokens or callback secrets. */
50
+ export declare function completeRemoteMcpAuthorization(options: CompleteRemoteMcpAuthorizationOptions): Promise<{
51
+ resource: string;
52
+ }>;
@@ -0,0 +1,133 @@
1
+ import { exchangeAuthorizationCode } from "./token-endpoint.js";
2
+ import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
3
+ import { normalizeOAuthScope } from "./scope.js";
4
+ const responseParameters = ["code", "state", "iss", "error", "error_description", "error_uri"];
5
+ function httpsUrl(value) {
6
+ const url = new URL(value);
7
+ if (url.protocol !== "https:" || url.username || url.password || url.href.includes("#"))
8
+ throw new Error();
9
+ return url;
10
+ }
11
+ function base64url(bytes) {
12
+ return btoa(String.fromCharCode(...bytes)).split("+").join("-").split("/").join("_").split("=").join("");
13
+ }
14
+ function timestamp(now) {
15
+ const value = now();
16
+ if (!Number.isSafeInteger(value) || value < 0 || value > 8_640_000_000_000_000 - 600_000)
17
+ throw new Error();
18
+ return value;
19
+ }
20
+ /** Start consent without a listener or a pending callback promise. Only the URL leaves private storage. */
21
+ export async function beginRemoteMcpAuthorization(options) {
22
+ try {
23
+ const { store, signal, discover } = options;
24
+ const now = options.now?.bind(options) ?? Date.now;
25
+ const resource = httpsUrl(options.resource).href;
26
+ const redirect = httpsUrl(options.redirectUri);
27
+ if (responseParameters.some(key => redirect.searchParams.has(key)))
28
+ throw new Error();
29
+ const client = structuredClone(options.client);
30
+ if (typeof client.clientId !== "string" || !client.clientId.trim())
31
+ throw new Error();
32
+ const method = normalizeOAuthTokenEndpointAuthMethod(client.tokenEndpointAuthMethod) ?? (client.clientSecret === undefined ? "none" : "client_secret_post");
33
+ if (client.clientSecret !== undefined && (typeof client.clientSecret !== "string" || !client.clientSecret.trim()))
34
+ throw new Error();
35
+ if (method !== "none" && client.clientSecret === undefined)
36
+ throw new Error();
37
+ if (client.registration !== undefined && (client.registration.client_id !== client.clientId ||
38
+ (client.registration.client_secret != null && client.registration.client_secret !== client.clientSecret) ||
39
+ (client.registration.token_endpoint_auth_method != null && client.registration.token_endpoint_auth_method !== method)))
40
+ throw new Error();
41
+ client.tokenEndpointAuthMethod = method;
42
+ if (client.requestedRedirectUri !== undefined && client.requestedRedirectUri !== redirect.href)
43
+ throw new Error();
44
+ if (client.registration?.redirect_uris != null && !client.registration.redirect_uris.includes(redirect.href))
45
+ throw new Error();
46
+ const scope = normalizeOAuthScope(options.scope);
47
+ const ttl = options.ttlMs ?? 600_000;
48
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 600_000)
49
+ throw new Error();
50
+ signal?.throwIfAborted();
51
+ const metadata = structuredClone(await discover(resource, signal));
52
+ signal?.throwIfAborted();
53
+ const server = metadata.authorizationServerMetadata;
54
+ if (httpsUrl(metadata.resource).href !== resource || httpsUrl(metadata.resourceMetadata.resource).href !== resource ||
55
+ server.issuer !== metadata.authorizationServer || !metadata.resourceMetadata.authorization_servers.includes(server.issuer) ||
56
+ !server.response_types_supported.includes("code") || !server.code_challenge_methods_supported.includes("S256"))
57
+ throw new Error();
58
+ httpsUrl(server.issuer);
59
+ httpsUrl(server.token_endpoint);
60
+ const authorization = httpsUrl(server.authorization_endpoint);
61
+ const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
62
+ const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
63
+ const challenge = base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier))));
64
+ const expiresAt = timestamp(now) + ttl;
65
+ const session = { resource, authorizationServer: server.issuer,
66
+ client: { ...client, registrationOwnership: "caller", requestedRedirectUri: redirect.href },
67
+ ...(scope === undefined ? {} : { requestedScope: scope }),
68
+ discovery: { resourceMetadataUrl: metadata.resourceMetadataUrl, resourceMetadata: metadata.resourceMetadata, authorizationServerMetadata: server } };
69
+ for (const [key, value] of Object.entries({ response_type: "code", client_id: client.clientId, redirect_uri: redirect.href,
70
+ resource, state, code_challenge: challenge, code_challenge_method: "S256" }))
71
+ authorization.searchParams.set(key, value);
72
+ authorization.searchParams.delete("scope");
73
+ if (scope !== undefined)
74
+ authorization.searchParams.set("scope", scope);
75
+ signal?.throwIfAborted();
76
+ await store.create({ state, expiresAt, redirectUri: redirect.href, codeVerifier,
77
+ requireIssuer: server.authorization_response_iss_parameter_supported === true, session });
78
+ signal?.throwIfAborted();
79
+ return { authorizationUrl: authorization.href, expiresAt };
80
+ }
81
+ catch {
82
+ throw new Error("Unable to begin MCP authorization");
83
+ }
84
+ }
85
+ /** Redeem once after any process restart, then conditionally persist. Returns no tokens or callback secrets. */
86
+ export async function completeRemoteMcpAuthorization(options) {
87
+ try {
88
+ const { store, fetch, signal } = options;
89
+ const now = options.now?.bind(options) ?? Date.now;
90
+ const callback = httpsUrl(options.callbackUrl);
91
+ for (const key of responseParameters)
92
+ if (callback.searchParams.getAll(key).length > 1)
93
+ throw new Error();
94
+ const state = callback.searchParams.get("state");
95
+ if (!state || state.length > 256)
96
+ throw new Error();
97
+ signal?.throwIfAborted();
98
+ const stored = await store.consume(state);
99
+ if (stored === null)
100
+ throw new Error();
101
+ const transaction = structuredClone(stored);
102
+ if (transaction.state !== state || !Number.isSafeInteger(transaction.expiresAt) || transaction.expiresAt <= timestamp(now))
103
+ throw new Error();
104
+ const expected = httpsUrl(transaction.redirectUri);
105
+ const issuer = callback.searchParams.get("iss");
106
+ if ((transaction.requireIssuer && issuer === null) || (issuer !== null && issuer !== transaction.session.authorizationServer))
107
+ throw new Error();
108
+ const code = callback.searchParams.get("code");
109
+ if (!code || callback.searchParams.has("error"))
110
+ throw new Error();
111
+ for (const key of responseParameters)
112
+ callback.searchParams.delete(key);
113
+ // Compare query pairs without changing their encoding/order requirements on the registered redirect.
114
+ const actualPairs = [...callback.searchParams].sort(), expectedPairs = [...expected.searchParams].sort();
115
+ if (callback.origin !== expected.origin || callback.pathname !== expected.pathname || JSON.stringify(actualPairs) !== JSON.stringify(expectedPairs))
116
+ throw new Error();
117
+ const session = structuredClone(transaction.session);
118
+ const tokenEndpoint = httpsUrl(String(session.discovery.authorizationServerMetadata.token_endpoint)).href;
119
+ signal?.throwIfAborted();
120
+ session.tokens = await exchangeAuthorizationCode({ tokenEndpoint, clientId: session.client.clientId,
121
+ clientSecret: session.client.clientSecret, tokenEndpointAuthMethod: session.client.tokenEndpointAuthMethod,
122
+ code, codeVerifier: transaction.codeVerifier, redirectUri: transaction.redirectUri, resource: session.resource, fetch, signal, now });
123
+ if (session.tokens.scope === undefined && session.requestedScope !== undefined)
124
+ session.tokens.scope = session.requestedScope;
125
+ signal?.throwIfAborted();
126
+ if (transaction.expiresAt <= timestamp(now) || !await store.commit(transaction, session))
127
+ throw new Error();
128
+ return { resource: session.resource };
129
+ }
130
+ catch {
131
+ throw new Error("Unable to complete MCP authorization");
132
+ }
133
+ }
@@ -89,7 +89,7 @@ async function requestTokens(input) {
89
89
  if (method === "client_secret_basic") {
90
90
  const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
91
91
  const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
92
- headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
92
+ headers.set("Authorization", `Basic ${btoa(`${encoded}:${encodedSecret}`)}`);
93
93
  }
94
94
  else {
95
95
  body.set("client_id", input.clientId);
@@ -10,3 +10,5 @@ import type { OAuthClientProvider, OAuthClientProviderOptions } from "./client/t
10
10
  export declare function createOAuthClientProvider(options: OAuthClientProviderOptions): OAuthClientProvider;
11
11
  export declare function createDefaultOAuthClientProvider(): never;
12
12
  export declare function createAuthStoreSessionStore(): never;
13
+ export { beginRemoteMcpAuthorization, completeRemoteMcpAuthorization } from "./client/resumable.js";
14
+ export type { RemoteMcpAuthorizationTransaction, RemoteMcpAuthorizationStore, BeginRemoteMcpAuthorizationOptions, CompleteRemoteMcpAuthorizationOptions } from "./client/resumable.js";
@@ -16,3 +16,4 @@ export function createDefaultOAuthClientProvider() {
16
16
  export function createAuthStoreSessionStore() {
17
17
  throw new Error("Desktop OAuth persistence is unavailable in Worker MCP");
18
18
  }
19
+ export { beginRemoteMcpAuthorization, completeRemoteMcpAuthorization } from "./client/resumable.js";
@@ -18,3 +18,5 @@ export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessTok
18
18
  export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
19
19
  export { readBoundedResponseText } from "./http-response.js";
20
20
  export { fetchMcpResponse } from "./http-fetch.js";
21
+ export { beginRemoteMcpAuthorization, completeRemoteMcpAuthorization } from "./client/resumable.js";
22
+ export type { RemoteMcpAuthorizationTransaction, RemoteMcpAuthorizationStore, BeginRemoteMcpAuthorizationOptions, CompleteRemoteMcpAuthorizationOptions } from "./client/resumable.js";
@@ -13,3 +13,4 @@ export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
13
13
  export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
14
14
  export { readBoundedResponseText } from "./http-response.js";
15
15
  export { fetchMcpResponse } from "./http-fetch.js";
16
+ export { beginRemoteMcpAuthorization, completeRemoteMcpAuthorization } from "./client/resumable.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3570,11 +3570,11 @@ function copyBoundedOAuthJson(value, message) {
3570
3570
  import http from "node:http";
3571
3571
 
3572
3572
  // ../mcp-oauth/dist/client/authorization-state.js
3573
- import crypto from "node:crypto";
3573
+ import crypto2 from "node:crypto";
3574
3574
  function createAuthorizationState(input) {
3575
3575
  const payload = {
3576
3576
  v: 1,
3577
- n: crypto.randomBytes(16).toString("base64url"),
3577
+ n: crypto2.randomBytes(16).toString("base64url"),
3578
3578
  i: input.issuer,
3579
3579
  r: input.requireIssuer
3580
3580
  };
@@ -4114,7 +4114,7 @@ function registrationMatchesRedirect(client, requestedUri, fresh = false) {
4114
4114
  }
4115
4115
 
4116
4116
  // ../mcp-oauth/dist/client/auth-store-session-store.js
4117
- import crypto2 from "node:crypto";
4117
+ import crypto3 from "node:crypto";
4118
4118
  import path4 from "node:path";
4119
4119
 
4120
4120
  // ../auth-store/dist/encrypted-file-store.js
@@ -4871,7 +4871,7 @@ function snapshotPersistenceOptions(options) {
4871
4871
  }
4872
4872
  function createNamedSecretStore(key2, options, defaults, namespace) {
4873
4873
  options = snapshotOAuthPersistenceOptions(options);
4874
- const hash = crypto2.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4874
+ const hash = crypto3.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4875
4875
  const configuredFilePath = options.fileStore?.filePath;
4876
4876
  const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4877
4877
  const fileStore = {
@@ -5184,12 +5184,12 @@ async function fetchMcpResponse(fetchImplementation, input, init = {}) {
5184
5184
  import { URL as URL2 } from "node:url";
5185
5185
 
5186
5186
  // ../mcp-oauth/dist/client/pkce.js
5187
- import crypto3 from "node:crypto";
5187
+ import crypto4 from "node:crypto";
5188
5188
  function generateCodeVerifier() {
5189
- return crypto3.randomBytes(32).toString("base64url");
5189
+ return crypto4.randomBytes(32).toString("base64url");
5190
5190
  }
5191
5191
  function generateCodeChallenge(verifier) {
5192
- return crypto3.createHash("sha256").update(verifier).digest("base64url");
5192
+ return crypto4.createHash("sha256").update(verifier).digest("base64url");
5193
5193
  }
5194
5194
 
5195
5195
  // ../mcp-oauth/dist/http-response.js
@@ -5322,7 +5322,7 @@ async function requestTokens(input) {
5322
5322
  if (method === "client_secret_basic") {
5323
5323
  const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
5324
5324
  const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
5325
- headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
5325
+ headers.set("Authorization", `Basic ${btoa(`${encoded}:${encodedSecret}`)}`);
5326
5326
  } else {
5327
5327
  body.set("client_id", input.clientId);
5328
5328
  if (method === "client_secret_post")
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.739",
11
+ "version": "0.0.741",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.739",
3
+ "version": "0.0.741",
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.739",
3
+ "version": "0.0.741",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -162,7 +162,7 @@
162
162
  "yaml"
163
163
  ],
164
164
  "optionalDependencies": {
165
- "toolcraft-schema": "0.0.739",
165
+ "toolcraft-schema": "0.0.741",
166
166
  "toolcraft-design": "*",
167
167
  "@poe-code/frontmatter": "*",
168
168
  "@poe-code/agent-mcp-config": "*",