toolcraft 0.0.349 → 0.0.351

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.78",
101
+ "version": "0.1.82",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.349",
116
+ "version": "0.0.351",
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.349",
126
+ "version": "0.0.351",
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.78",
101
+ "version": "0.1.82",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.349",
116
+ "version": "0.0.351",
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.349",
126
+ "version": "0.0.351",
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.78",
21
+ "version": "0.1.82",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -61,12 +61,16 @@ alone do not establish either native error type.
61
61
  - `browser.timeoutMs` optional authorization deadline (default 120,000 ms)
62
62
  - `sessionStore` optional
63
63
  - `authStore` optional `auth-store` backend config for the default session store
64
- - `now()` optional clock override
64
+ - `now()` optional clock override returning integer Unix epoch milliseconds within the JavaScript Date range
65
65
 
66
66
  The default provider captures client, interaction, callback, landing-page and
67
67
  lock policies when created. Create a new provider to change those settings.
68
68
  Selected callbacks, stores, clocks and AbortSignals remain live host dependencies;
69
69
  aborting the original signal still cancels authorization.
70
+ An invalid clock fails before attaching or refreshing a grant with a known
71
+ expiry or redeeming a time-limited client secret. Unknown access lifetimes and
72
+ unlimited secret lifetimes do not require a clock read. Valid negative epoch
73
+ times remain supported.
70
74
  Native session and client persistence factories also capture file paths, salts,
71
75
  Keychain identities, lock locations and selected filesystem/command handles.
72
76
  Mutating these settings cannot redirect a later read or write.
@@ -94,6 +98,9 @@ authorization and code exchange. Credentials, fragments, port zero and reserved
94
98
  OAuth callback query parameters, including `error_uri`, are rejected before binding a listener.
95
99
  Fragment rejection includes a trailing empty `#`. Empty queries and percent-escaped
96
100
  hash data remain valid and keep their exact registered spelling.
101
+ Direct initial grants and host-supplied or stored authorization, token and
102
+ registration endpoints also reject empty fragment components before consent or
103
+ credential redemption. Percent-escaped hashes remain ordinary path/query data.
97
104
  `createDefaultOAuthClientProvider` also checks the configured redirect before
98
105
  creating the provider. Imported tokens that cannot be sent as HTTP header
99
106
  values fail with diagnostics that omit their contents. Omit
@@ -164,7 +171,14 @@ refresh retains the previous granted set. Mismatched responses never activate
164
171
  credentials; an unusable refresh response retains the pending refresh record.
165
172
  `resourceIdentity` selects a native-owned logical server within its optional
166
173
  persistence namespace. One encrypted, locked document owns that identity's
167
- current resource URL, sessions and registrations. Changing the URL retires its
174
+ current resource URL, sessions and registrations. Explicit imports and resets
175
+ reject fragment components, including an empty trailing `#`; imported protected-
176
+ resource metadata and stored client issuers obey the same fragment-free policy.
177
+ Issuer identifiers must also omit query components, including an empty `?`.
178
+ Direct and stored authorization flows enforce the same issuer URL policy before
179
+ consent or token redemption. New discovery identity and endpoint policy checks
180
+ run before acquiring the session transaction, so invalid replacement metadata
181
+ cannot retire a valid old grant. Escaped delimiter data stays valid. Changing the URL retires its
168
182
  credentials permanently; returning to the old URL does not restore them.
169
183
  Retired identities also withhold stale initial grants; authorize again or select
170
184
  a fresh explicit profile to import a new grant. This option requires native persistence; custom
@@ -187,7 +201,9 @@ using the original configured client. Persisted sessions take precedence,
187
201
  including sessions whose tokens have been cleared; an import cannot revive them.
188
202
  Input tokens are copied before any host clock anchors a relative lifetime;
189
203
  clock mutation cannot replace the selected access/refresh/scope values. Invalid
190
- expiry values fail before authorization.
204
+ expiry values fail before authorization. Relative-lifetime clock anchors must
205
+ also be valid epoch milliseconds; adding the lifetime cannot make an invalid
206
+ issuance clock acceptable. This applies to raw-grant parsing and token responses.
191
207
  For a raw OAuth response, `parseOAuthTokenGrant(response, { issuedAt, expiresAt })`
192
208
  returns normalized `StoredOAuthTokens`. Timing options are captured before the
193
209
  host clock runs, so it cannot bypass their earlier validation. The parser accepts
@@ -40,7 +40,13 @@ export function createDefaultOAuthClientProvider(options) {
40
40
  const sessionStore = resourceStores?.sessionStore ?? options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
41
41
  const clientStore = resourceStores?.clientStore ??
42
42
  (options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace));
43
- const now = options.now ?? Date.now;
43
+ const clock = options.now ?? Date.now;
44
+ const now = () => {
45
+ const timestamp = clock();
46
+ if (!Number.isSafeInteger(timestamp) || Math.abs(timestamp) > MAX_JS_DATE_MS)
47
+ throw new Error("OAuth clock must return valid epoch milliseconds");
48
+ return timestamp;
49
+ };
44
50
  const registeredClients = new Map();
45
51
  if (options.initialGrant !== undefined) {
46
52
  let resource;
@@ -50,7 +56,7 @@ export function createDefaultOAuthClientProvider(options) {
50
56
  catch {
51
57
  throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
52
58
  }
53
- if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.hash)
59
+ if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.href.includes("#"))
54
60
  throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
55
61
  }
56
62
  const initialGrant = options.initialGrant === undefined ? undefined : {
@@ -166,6 +172,11 @@ export function createDefaultOAuthClientProvider(options) {
166
172
  };
167
173
  async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
168
174
  signal?.throwIfAborted();
175
+ if (discovery !== undefined) {
176
+ if (getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer)
177
+ throw new Error("OAuth discovery authorization-server issuer mismatch");
178
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
179
+ }
169
180
  const canonicalResource = canonicalizeResourceIndicator(resource);
170
181
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
171
182
  let session = await loadSession(canonicalResource);
@@ -181,9 +192,6 @@ export function createDefaultOAuthClientProvider(options) {
181
192
  if (session !== null && initialGrant?.resource === canonicalResource)
182
193
  initialGrantConsumed = true;
183
194
  signal?.throwIfAborted();
184
- if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
185
- throw new Error("OAuth discovery authorization-server issuer mismatch");
186
- }
187
195
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
188
196
  || getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
189
197
  || (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
@@ -192,7 +200,6 @@ export function createDefaultOAuthClientProvider(options) {
192
200
  }
193
201
  if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
194
202
  initialGrant.tokens !== undefined && initialGrant.client !== null) {
195
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
196
203
  session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
197
204
  client: initialGrant.client, tokens: initialGrant.tokens,
198
205
  ...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
@@ -756,18 +763,21 @@ function isLoopbackHostname(hostname) {
756
763
  }
757
764
  function assertSecureUrl(value, label) {
758
765
  const url = new URL(value);
759
- if (url.username !== "" || url.password !== "" || url.hash !== "") {
766
+ if (url.username !== "" || url.password !== "" || url.href.includes("#")) {
760
767
  throw new Error(`${label} must not include credentials or fragment`);
761
768
  }
762
769
  if (url.protocol === "https:") {
763
- return;
770
+ return url;
764
771
  }
765
772
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
766
- return;
773
+ return url;
767
774
  }
768
775
  throw new Error(`${label} must use https unless it targets a loopback host`);
769
776
  }
770
777
  function assertSecureOAuthFlowEndpoints(metadata) {
778
+ const issuer = assertSecureUrl(requireOwnString(metadata, "issuer", "Authorization server metadata"), "Authorization server issuer");
779
+ if (issuer.href.includes("?"))
780
+ throw new Error("Authorization server issuer must not include query or fragment");
771
781
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
772
782
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
773
783
  const registrationEndpoint = getOwnString(metadata, "registration_endpoint");
@@ -18,7 +18,7 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
18
18
  catch {
19
19
  throw new Error("OAuth reset resource must be an absolute HTTP URL");
20
20
  }
21
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
21
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("#"))
22
22
  throw new Error("OAuth reset resource must be an HTTP URL without credentials or fragment");
23
23
  await replace({ version: 1, resource: canonicalizeResourceIndicator(url), generation: 1, session: null, clients: {} }, options);
24
24
  },
@@ -28,11 +28,12 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
28
28
  try {
29
29
  if (!isStoredOAuthSession(session) || session.tokens === undefined || session.refreshState !== undefined)
30
30
  throw new Error("Invalid session");
31
- const resource = new URL(session.resource), issuer = new URL(session.authorizationServer);
32
- if ([resource, issuer].some(url => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) ||
33
- session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer ||
34
- typeof session.discovery.resourceMetadata.resource !== "string" ||
35
- canonicalizeResourceIndicator(session.discovery.resourceMetadata.resource) !== canonicalizeResourceIndicator(resource))
31
+ if (typeof session.discovery.resourceMetadata.resource !== "string")
32
+ throw new Error("Invalid resource metadata");
33
+ const resource = new URL(session.resource), issuer = new URL(session.authorizationServer), advertisedResource = new URL(session.discovery.resourceMetadata.resource);
34
+ if ([resource, issuer, advertisedResource].some(url => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("#")) ||
35
+ issuer.href.includes("?") || session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer ||
36
+ canonicalizeResourceIndicator(advertisedResource) !== canonicalizeResourceIndicator(resource))
36
37
  throw new Error("Invalid binding");
37
38
  session.client = normalizeStoredOAuthClient(session.client);
38
39
  if (session.client.registration !== undefined)
@@ -92,7 +93,7 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
92
93
  catch {
93
94
  throw new Error("Invalid stored OAuth resource identity URL");
94
95
  }
95
- if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
96
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.href.includes("#"))
96
97
  throw new Error("Invalid stored OAuth resource identity URL");
97
98
  for (const [issuer, client] of Object.entries(record.clients)) {
98
99
  const normalized = normalizeStoredOAuthClient(client);
@@ -103,7 +104,7 @@ export function createResourceBoundOAuthStores(options, namespace, identity) {
103
104
  catch {
104
105
  throw new Error("Invalid stored OAuth resource client issuer");
105
106
  }
106
- if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
107
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("?") || url.href.includes("#"))
107
108
  throw new Error("Invalid stored OAuth resource client");
108
109
  Object.defineProperty(record.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
109
110
  }
@@ -130,7 +130,10 @@ async function requestTokens(input) {
130
130
  expiresIn < 0) {
131
131
  throw new Error("OAuth token response has invalid expires_in");
132
132
  }
133
- expiresAt = input.now() + expiresIn * 1000;
133
+ const issuedAt = input.now();
134
+ if (!Number.isSafeInteger(issuedAt) || Math.abs(issuedAt) > MAX_JS_DATE_MS)
135
+ throw new Error("OAuth token response has invalid expires_in");
136
+ expiresAt = issuedAt + expiresIn * 1000;
134
137
  if (!Number.isSafeInteger(expiresAt) ||
135
138
  expiresAt > MAX_JS_DATE_MS ||
136
139
  !Number.isFinite(new Date(expiresAt).getTime())) {
@@ -34,7 +34,10 @@ export function parseOAuthTokenGrant(value, options = {}) {
34
34
  const absoluteSeconds = typeof seconds === "number" ? seconds * 1000 : undefined;
35
35
  if (absoluteSeconds !== undefined && !validTimestamp(absoluteSeconds))
36
36
  throw invalid();
37
- const relative = typeof lifetime === "number" ? (options.issuedAt ?? (options.now ?? Date.now)()) + lifetime * 1000 : undefined;
37
+ const anchor = typeof lifetime === "number" ? options.issuedAt ?? (options.now ?? Date.now)() : undefined;
38
+ if (typeof lifetime === "number" && !validTimestamp(anchor))
39
+ throw invalid();
40
+ const relative = typeof lifetime === "number" ? anchor + lifetime * 1000 : undefined;
38
41
  if (relative !== undefined && !validTimestamp(relative))
39
42
  throw invalid();
40
43
  const expiresAt = options.expiresAt ?? (typeof milliseconds === "number" ? milliseconds : undefined) ?? absoluteSeconds ?? relative ?? null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.78",
3
+ "version": "0.1.82",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -63,6 +63,11 @@ embedded credentials or fragments; endpoint changes close the connection.
63
63
  Empty fragment delimiters are rejected too; escaped hashes in an endpoint path
64
64
  or query remain literal data.
65
65
  Both transports accept the same headers, OAuth provider and response limits.
66
+ The original HTTP transport target must be an absolute HTTP/HTTPS URL without
67
+ credentials or a fragment, including an empty trailing `#`. Invalid targets fail
68
+ before OAuth provider construction or clock reads. Empty queries and escaped
69
+ path/query delimiters remain valid; accepted target strings keep their spelling.
70
+
66
71
  Static HTTP headers are copied and validated when the transport is constructed,
67
72
  before OAuth callbacks run. Invalid headers fail without reflecting their values.
68
73
  Use `snapshotHttpTransportHeaders(headers)` to apply the same ownership and safe
@@ -4801,7 +4801,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4801
4801
  } catch {
4802
4802
  throw new Error("OAuth reset resource must be an absolute HTTP URL");
4803
4803
  }
4804
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4804
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("#"))
4805
4805
  throw new Error("OAuth reset resource must be an HTTP URL without credentials or fragment");
4806
4806
  await replace({ version: 1, resource: canonicalizeResourceIndicator(url), generation: 1, session: null, clients: {} }, options2);
4807
4807
  },
@@ -4811,8 +4811,10 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4811
4811
  try {
4812
4812
  if (!isStoredOAuthSession(session) || session.tokens === void 0 || session.refreshState !== void 0)
4813
4813
  throw new Error("Invalid session");
4814
- const resource = new URL(session.resource), issuer = new URL(session.authorizationServer);
4815
- if ([resource, issuer].some((url) => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) || session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer || typeof session.discovery.resourceMetadata.resource !== "string" || canonicalizeResourceIndicator(session.discovery.resourceMetadata.resource) !== canonicalizeResourceIndicator(resource))
4814
+ if (typeof session.discovery.resourceMetadata.resource !== "string")
4815
+ throw new Error("Invalid resource metadata");
4816
+ const resource = new URL(session.resource), issuer = new URL(session.authorizationServer), advertisedResource = new URL(session.discovery.resourceMetadata.resource);
4817
+ if ([resource, issuer, advertisedResource].some((url) => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("#")) || issuer.href.includes("?") || session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer || canonicalizeResourceIndicator(advertisedResource) !== canonicalizeResourceIndicator(resource))
4816
4818
  throw new Error("Invalid binding");
4817
4819
  session.client = normalizeStoredOAuthClient(session.client);
4818
4820
  if (session.client.registration !== void 0)
@@ -4868,7 +4870,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4868
4870
  } catch {
4869
4871
  throw new Error("Invalid stored OAuth resource identity URL");
4870
4872
  }
4871
- if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
4873
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.href.includes("#"))
4872
4874
  throw new Error("Invalid stored OAuth resource identity URL");
4873
4875
  for (const [issuer, client] of Object.entries(record2.clients)) {
4874
4876
  const normalized = normalizeStoredOAuthClient(client);
@@ -4878,7 +4880,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4878
4880
  } catch {
4879
4881
  throw new Error("Invalid stored OAuth resource client issuer");
4880
4882
  }
4881
- if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4883
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("?") || url.href.includes("#"))
4882
4884
  throw new Error("Invalid stored OAuth resource client");
4883
4885
  Object.defineProperty(record2.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
4884
4886
  }
@@ -5178,7 +5180,10 @@ async function requestTokens(input) {
5178
5180
  if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || !Number.isInteger(expiresIn) || expiresIn < 0) {
5179
5181
  throw new Error("OAuth token response has invalid expires_in");
5180
5182
  }
5181
- expiresAt = input.now() + expiresIn * 1e3;
5183
+ const issuedAt = input.now();
5184
+ if (!Number.isSafeInteger(issuedAt) || Math.abs(issuedAt) > MAX_JS_DATE_MS2)
5185
+ throw new Error("OAuth token response has invalid expires_in");
5186
+ expiresAt = issuedAt + expiresIn * 1e3;
5182
5187
  if (!Number.isSafeInteger(expiresAt) || expiresAt > MAX_JS_DATE_MS2 || !Number.isFinite(new Date(expiresAt).getTime())) {
5183
5188
  throw new Error("OAuth token response has invalid expires_in");
5184
5189
  }
@@ -5335,7 +5340,13 @@ function createDefaultOAuthClientProvider(options) {
5335
5340
  const resourceStores = options.resourceIdentity === void 0 ? void 0 : createResourceBoundOAuthStores(options.authStore ?? {}, options.persistenceNamespace, options.resourceIdentity);
5336
5341
  const sessionStore = resourceStores?.sessionStore ?? options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
5337
5342
  const clientStore = resourceStores?.clientStore ?? (options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace));
5338
- const now = options.now ?? Date.now;
5343
+ const clock = options.now ?? Date.now;
5344
+ const now = () => {
5345
+ const timestamp = clock();
5346
+ if (!Number.isSafeInteger(timestamp) || Math.abs(timestamp) > MAX_JS_DATE_MS3)
5347
+ throw new Error("OAuth clock must return valid epoch milliseconds");
5348
+ return timestamp;
5349
+ };
5339
5350
  const registeredClients = /* @__PURE__ */ new Map();
5340
5351
  if (options.initialGrant !== void 0) {
5341
5352
  let resource;
@@ -5344,7 +5355,7 @@ function createDefaultOAuthClientProvider(options) {
5344
5355
  } catch {
5345
5356
  throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
5346
5357
  }
5347
- if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.hash)
5358
+ if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.href.includes("#"))
5348
5359
  throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
5349
5360
  }
5350
5361
  const initialGrant = options.initialGrant === void 0 ? void 0 : {
@@ -5456,6 +5467,11 @@ function createDefaultOAuthClientProvider(options) {
5456
5467
  };
5457
5468
  async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
5458
5469
  signal?.throwIfAborted();
5470
+ if (discovery !== void 0) {
5471
+ if (getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer)
5472
+ throw new Error("OAuth discovery authorization-server issuer mismatch");
5473
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
5474
+ }
5459
5475
  const canonicalResource = canonicalizeResourceIndicator(resource);
5460
5476
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5461
5477
  let session = await loadSession(canonicalResource);
@@ -5471,15 +5487,11 @@ function createDefaultOAuthClientProvider(options) {
5471
5487
  if (session !== null && initialGrant?.resource === canonicalResource)
5472
5488
  initialGrantConsumed = true;
5473
5489
  signal?.throwIfAborted();
5474
- if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
5475
- throw new Error("OAuth discovery authorization-server issuer mismatch");
5476
- }
5477
5490
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
5478
5491
  await clearSession(canonicalResource);
5479
5492
  session = null;
5480
5493
  }
5481
5494
  if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
5482
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
5483
5495
  session = {
5484
5496
  resource: canonicalResource,
5485
5497
  authorizationServer: discovery.authorizationServer,
@@ -6011,18 +6023,21 @@ function isLoopbackHostname(hostname2) {
6011
6023
  }
6012
6024
  function assertSecureUrl(value, label) {
6013
6025
  const url = new URL2(value);
6014
- if (url.username !== "" || url.password !== "" || url.hash !== "") {
6026
+ if (url.username !== "" || url.password !== "" || url.href.includes("#")) {
6015
6027
  throw new Error(`${label} must not include credentials or fragment`);
6016
6028
  }
6017
6029
  if (url.protocol === "https:") {
6018
- return;
6030
+ return url;
6019
6031
  }
6020
6032
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
6021
- return;
6033
+ return url;
6022
6034
  }
6023
6035
  throw new Error(`${label} must use https unless it targets a loopback host`);
6024
6036
  }
6025
6037
  function assertSecureOAuthFlowEndpoints(metadata) {
6038
+ const issuer = assertSecureUrl(requireOwnString(metadata, "issuer", "Authorization server metadata"), "Authorization server issuer");
6039
+ if (issuer.href.includes("?"))
6040
+ throw new Error("Authorization server issuer must not include query or fragment");
6026
6041
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
6027
6042
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
6028
6043
  const registrationEndpoint = getOwnString2(metadata, "registration_endpoint");
@@ -7741,6 +7756,13 @@ var HttpTransport = class {
7741
7756
  }) {
7742
7757
  if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1)
7743
7758
  throw new Error("HTTP response byte limit must be a positive safe integer");
7759
+ try {
7760
+ const target = new URL(url);
7761
+ if (!["http:", "https:"].includes(target.protocol) || target.username || target.password || target.href.includes("#"))
7762
+ throw new Error("Invalid HTTP transport target");
7763
+ } catch {
7764
+ throw new Error("HTTP transport URL must be an absolute HTTP URL without credentials or fragment");
7765
+ }
7744
7766
  this.maxResponseBytes = maxResponseBytes;
7745
7767
  this.url = url;
7746
7768
  this.mode = mode;
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.349",
11
+ "version": "0.0.351",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.349",
3
+ "version": "0.0.351",
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.349",
3
+ "version": "0.0.351",
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.349",
161
+ "toolcraft-schema": "0.0.351",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",