toolcraft 0.0.350 → 0.0.352

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.80",
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.350",
116
+ "version": "0.0.352",
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.350",
126
+ "version": "0.0.352",
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.80",
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.350",
116
+ "version": "0.0.352",
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.350",
126
+ "version": "0.0.352",
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.80",
21
+ "version": "0.1.82",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -123,6 +123,9 @@ A browser callback cannot redirect a later code exchange by changing caller
123
123
  metadata. Metadata loaded from a host session store is copied before clock or
124
124
  fetch callbacks, retaining the original refresh endpoint and persisted extensions. Explicit authentication owns metadata when lazy discovery returns;
125
125
  the selected discovery method retains its original receiver and live host state.
126
+ Browser and standalone loopback snapshots retain declared redirects, deadlines,
127
+ cancellation and selected host hooks regardless of property enumerability.
128
+ Browser callback hooks retain their original receiver and live host state.
126
129
 
127
130
  Provider request inputs accept an optional `signal`. It reaches callback waits,
128
131
  registration, token requests and bounded token-body reads. Cancellation retains
@@ -171,7 +174,14 @@ refresh retains the previous granted set. Mismatched responses never activate
171
174
  credentials; an unusable refresh response retains the pending refresh record.
172
175
  `resourceIdentity` selects a native-owned logical server within its optional
173
176
  persistence namespace. One encrypted, locked document owns that identity's
174
- current resource URL, sessions and registrations. Changing the URL retires its
177
+ current resource URL, sessions and registrations. Explicit imports and resets
178
+ reject fragment components, including an empty trailing `#`; imported protected-
179
+ resource metadata and stored client issuers obey the same fragment-free policy.
180
+ Issuer identifiers must also omit query components, including an empty `?`.
181
+ Direct and stored authorization flows enforce the same issuer URL policy before
182
+ consent or token redemption. New discovery identity and endpoint policy checks
183
+ run before acquiring the session transaction, so invalid replacement metadata
184
+ cannot retire a valid old grant. Escaped delimiter data stays valid. Changing the URL retires its
175
185
  credentials permanently; returning to the old URL does not restore them.
176
186
  Retired identities also withhold stale initial grants; authorize again or select
177
187
  a fresh explicit profile to import a new grant. This option requires native persistence; custom
@@ -194,7 +204,9 @@ using the original configured client. Persisted sessions take precedence,
194
204
  including sessions whose tokens have been cleared; an import cannot revive them.
195
205
  Input tokens are copied before any host clock anchors a relative lifetime;
196
206
  clock mutation cannot replace the selected access/refresh/scope values. Invalid
197
- expiry values fail before authorization.
207
+ expiry values fail before authorization. Relative-lifetime clock anchors must
208
+ also be valid epoch milliseconds; adding the lifetime cannot make an invalid
209
+ issuance clock acceptable. This applies to raw-grant parsing and token responses.
198
210
  For a raw OAuth response, `parseOAuthTokenGrant(response, { issuedAt, expiresAt })`
199
211
  returns normalized `StoredOAuthTokens`. Timing options are captured before the
200
212
  host clock runs, so it cannot bypass their earlier validation. The parser accepts
@@ -6,7 +6,7 @@ import { isIP } from "node:net";
6
6
  import { fetchMcpResponse } from "../http-fetch.js";
7
7
  import { URL } from "node:url";
8
8
  import { createAuthStoreClientStore, createAuthStoreSessionStore, assertPersistenceNamespace } from "./auth-store-session-store.js";
9
- import { createLoopbackAuthorizationSession, loopbackTarget } from "./loopback-authorization.js";
9
+ import { createLoopbackAuthorizationSession, loopbackTarget, snapshotLoopbackAuthorizationOptions } from "./loopback-authorization.js";
10
10
  import { createAuthorizationState } from "./authorization-state.js";
11
11
  import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
12
12
  import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
@@ -21,8 +21,7 @@ export function createOAuthClientProvider(options) {
21
21
  return createDefaultOAuthClientProvider(options);
22
22
  }
23
23
  export function createDefaultOAuthClientProvider(options) {
24
- const browser = { ...options.browser,
25
- ...(options.browser.landingPage === undefined ? {} : { landingPage: { ...options.browser.landingPage } }) };
24
+ const browser = snapshotLoopbackAuthorizationOptions(options.browser);
26
25
  const clientMode = options.client.mode;
27
26
  const interactiveEnabled = options.allowInteractive !== false;
28
27
  const sessionLockTimeoutMs = options.sessionLockTimeoutMs;
@@ -172,6 +171,11 @@ export function createDefaultOAuthClientProvider(options) {
172
171
  };
173
172
  async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
174
173
  signal?.throwIfAborted();
174
+ if (discovery !== undefined) {
175
+ if (getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer)
176
+ throw new Error("OAuth discovery authorization-server issuer mismatch");
177
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
178
+ }
175
179
  const canonicalResource = canonicalizeResourceIndicator(resource);
176
180
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
177
181
  let session = await loadSession(canonicalResource);
@@ -187,9 +191,6 @@ export function createDefaultOAuthClientProvider(options) {
187
191
  if (session !== null && initialGrant?.resource === canonicalResource)
188
192
  initialGrantConsumed = true;
189
193
  signal?.throwIfAborted();
190
- if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
191
- throw new Error("OAuth discovery authorization-server issuer mismatch");
192
- }
193
194
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
194
195
  || getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
195
196
  || (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
@@ -198,7 +199,6 @@ export function createDefaultOAuthClientProvider(options) {
198
199
  }
199
200
  if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
200
201
  initialGrant.tokens !== undefined && initialGrant.client !== null) {
201
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
202
202
  session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
203
203
  client: initialGrant.client, tokens: initialGrant.tokens,
204
204
  ...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
@@ -766,14 +766,17 @@ function assertSecureUrl(value, label) {
766
766
  throw new Error(`${label} must not include credentials or fragment`);
767
767
  }
768
768
  if (url.protocol === "https:") {
769
- return;
769
+ return url;
770
770
  }
771
771
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
772
- return;
772
+ return url;
773
773
  }
774
774
  throw new Error(`${label} must use https unless it targets a loopback host`);
775
775
  }
776
776
  function assertSecureOAuthFlowEndpoints(metadata) {
777
+ const issuer = assertSecureUrl(requireOwnString(metadata, "issuer", "Authorization server metadata"), "Authorization server issuer");
778
+ if (issuer.href.includes("?"))
779
+ throw new Error("Authorization server issuer must not include query or fragment");
777
780
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
778
781
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
779
782
  const registrationEndpoint = getOwnString(metadata, "registration_endpoint");
@@ -20,6 +20,8 @@ export interface LoopbackAuthorizationSession {
20
20
  waitForCode(authorizationUrl: string): Promise<string>;
21
21
  close(): void;
22
22
  }
23
+ /** Capture declared configuration regardless of property enumerability. */
24
+ export declare function snapshotLoopbackAuthorizationOptions(options: LoopbackAuthorizationOptions): LoopbackAuthorizationOptions;
23
25
  /** Authorization callback denial, retaining the provider diagnostic for host observers. */
24
26
  export declare class OAuthAuthorizationError extends Error {
25
27
  readonly error: string;
@@ -2,6 +2,12 @@ import http from "node:http";
2
2
  import { parseAuthorizationState } from "./authorization-state.js";
3
3
  const authorizationErrorBrand = Symbol.for("poe-platform.mcp-oauth.OAuthAuthorizationError");
4
4
  const oauthCallbackParameters = ["code", "state", "iss", "error", "error_description", "error_uri"];
5
+ /** Capture declared configuration regardless of property enumerability. */
6
+ export function snapshotLoopbackAuthorizationOptions(options) {
7
+ return { ...options, openBrowser: options.openBrowser?.bind(options), readLine: options.readLine?.bind(options), createServer: options.createServer?.bind(options),
8
+ callbackPath: options.callbackPath, redirectUri: options.redirectUri, signal: options.signal, timeoutMs: options.timeoutMs,
9
+ landingPage: options.landingPage === undefined ? undefined : { ...options.landingPage } };
10
+ }
5
11
  /** Authorization callback denial, retaining the provider diagnostic for host observers. */
6
12
  export class OAuthAuthorizationError extends Error {
7
13
  error;
@@ -19,10 +25,7 @@ export class OAuthAuthorizationError extends Error {
19
25
  }
20
26
  }
21
27
  export async function createLoopbackAuthorizationSession(options = {}) {
22
- const selected = { ...options };
23
- options = { ...selected, createServer: selected.createServer?.bind(options),
24
- openBrowser: selected.openBrowser?.bind(options), readLine: selected.readLine?.bind(options),
25
- landingPage: selected.landingPage === undefined ? undefined : { ...selected.landingPage } };
28
+ options = snapshotLoopbackAuthorizationOptions(options);
26
29
  options.signal?.throwIfAborted();
27
30
  const timeoutMs = options.timeoutMs ?? 120_000;
28
31
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
@@ -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.80",
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
@@ -3518,6 +3518,19 @@ function getOwnEntry(record2, key2) {
3518
3518
  // ../mcp-oauth/dist/client/loopback-authorization.js
3519
3519
  var authorizationErrorBrand = /* @__PURE__ */ Symbol.for("poe-platform.mcp-oauth.OAuthAuthorizationError");
3520
3520
  var oauthCallbackParameters = ["code", "state", "iss", "error", "error_description", "error_uri"];
3521
+ function snapshotLoopbackAuthorizationOptions(options) {
3522
+ return {
3523
+ ...options,
3524
+ openBrowser: options.openBrowser?.bind(options),
3525
+ readLine: options.readLine?.bind(options),
3526
+ createServer: options.createServer?.bind(options),
3527
+ callbackPath: options.callbackPath,
3528
+ redirectUri: options.redirectUri,
3529
+ signal: options.signal,
3530
+ timeoutMs: options.timeoutMs,
3531
+ landingPage: options.landingPage === void 0 ? void 0 : { ...options.landingPage }
3532
+ };
3533
+ }
3521
3534
  var OAuthAuthorizationError = class extends Error {
3522
3535
  error;
3523
3536
  errorDescription;
@@ -3534,14 +3547,7 @@ var OAuthAuthorizationError = class extends Error {
3534
3547
  }
3535
3548
  };
3536
3549
  async function createLoopbackAuthorizationSession(options = {}) {
3537
- const selected = { ...options };
3538
- options = {
3539
- ...selected,
3540
- createServer: selected.createServer?.bind(options),
3541
- openBrowser: selected.openBrowser?.bind(options),
3542
- readLine: selected.readLine?.bind(options),
3543
- landingPage: selected.landingPage === void 0 ? void 0 : { ...selected.landingPage }
3544
- };
3550
+ options = snapshotLoopbackAuthorizationOptions(options);
3545
3551
  options.signal?.throwIfAborted();
3546
3552
  const timeoutMs = options.timeoutMs ?? 12e4;
3547
3553
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
@@ -4801,7 +4807,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4801
4807
  } catch {
4802
4808
  throw new Error("OAuth reset resource must be an absolute HTTP URL");
4803
4809
  }
4804
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4810
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("#"))
4805
4811
  throw new Error("OAuth reset resource must be an HTTP URL without credentials or fragment");
4806
4812
  await replace({ version: 1, resource: canonicalizeResourceIndicator(url), generation: 1, session: null, clients: {} }, options2);
4807
4813
  },
@@ -4811,8 +4817,10 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4811
4817
  try {
4812
4818
  if (!isStoredOAuthSession(session) || session.tokens === void 0 || session.refreshState !== void 0)
4813
4819
  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))
4820
+ if (typeof session.discovery.resourceMetadata.resource !== "string")
4821
+ throw new Error("Invalid resource metadata");
4822
+ const resource = new URL(session.resource), issuer = new URL(session.authorizationServer), advertisedResource = new URL(session.discovery.resourceMetadata.resource);
4823
+ 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
4824
  throw new Error("Invalid binding");
4817
4825
  session.client = normalizeStoredOAuthClient(session.client);
4818
4826
  if (session.client.registration !== void 0)
@@ -4868,7 +4876,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4868
4876
  } catch {
4869
4877
  throw new Error("Invalid stored OAuth resource identity URL");
4870
4878
  }
4871
- if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
4879
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.href.includes("#"))
4872
4880
  throw new Error("Invalid stored OAuth resource identity URL");
4873
4881
  for (const [issuer, client] of Object.entries(record2.clients)) {
4874
4882
  const normalized = normalizeStoredOAuthClient(client);
@@ -4878,7 +4886,7 @@ function createResourceBoundOAuthStores(options, namespace, identity) {
4878
4886
  } catch {
4879
4887
  throw new Error("Invalid stored OAuth resource client issuer");
4880
4888
  }
4881
- if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4889
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.includes("?") || url.href.includes("#"))
4882
4890
  throw new Error("Invalid stored OAuth resource client");
4883
4891
  Object.defineProperty(record2.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
4884
4892
  }
@@ -5178,7 +5186,10 @@ async function requestTokens(input) {
5178
5186
  if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || !Number.isInteger(expiresIn) || expiresIn < 0) {
5179
5187
  throw new Error("OAuth token response has invalid expires_in");
5180
5188
  }
5181
- expiresAt = input.now() + expiresIn * 1e3;
5189
+ const issuedAt = input.now();
5190
+ if (!Number.isSafeInteger(issuedAt) || Math.abs(issuedAt) > MAX_JS_DATE_MS2)
5191
+ throw new Error("OAuth token response has invalid expires_in");
5192
+ expiresAt = issuedAt + expiresIn * 1e3;
5182
5193
  if (!Number.isSafeInteger(expiresAt) || expiresAt > MAX_JS_DATE_MS2 || !Number.isFinite(new Date(expiresAt).getTime())) {
5183
5194
  throw new Error("OAuth token response has invalid expires_in");
5184
5195
  }
@@ -5316,10 +5327,7 @@ function createOAuthClientProvider(options) {
5316
5327
  return createDefaultOAuthClientProvider(options);
5317
5328
  }
5318
5329
  function createDefaultOAuthClientProvider(options) {
5319
- const browser = {
5320
- ...options.browser,
5321
- ...options.browser.landingPage === void 0 ? {} : { landingPage: { ...options.browser.landingPage } }
5322
- };
5330
+ const browser = snapshotLoopbackAuthorizationOptions(options.browser);
5323
5331
  const clientMode = options.client.mode;
5324
5332
  const interactiveEnabled = options.allowInteractive !== false;
5325
5333
  const sessionLockTimeoutMs = options.sessionLockTimeoutMs;
@@ -5462,6 +5470,11 @@ function createDefaultOAuthClientProvider(options) {
5462
5470
  };
5463
5471
  async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
5464
5472
  signal?.throwIfAborted();
5473
+ if (discovery !== void 0) {
5474
+ if (getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer)
5475
+ throw new Error("OAuth discovery authorization-server issuer mismatch");
5476
+ assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
5477
+ }
5465
5478
  const canonicalResource = canonicalizeResourceIndicator(resource);
5466
5479
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5467
5480
  let session = await loadSession(canonicalResource);
@@ -5477,15 +5490,11 @@ function createDefaultOAuthClientProvider(options) {
5477
5490
  if (session !== null && initialGrant?.resource === canonicalResource)
5478
5491
  initialGrantConsumed = true;
5479
5492
  signal?.throwIfAborted();
5480
- if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
5481
- throw new Error("OAuth discovery authorization-server issuer mismatch");
5482
- }
5483
5493
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
5484
5494
  await clearSession(canonicalResource);
5485
5495
  session = null;
5486
5496
  }
5487
5497
  if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
5488
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
5489
5498
  session = {
5490
5499
  resource: canonicalResource,
5491
5500
  authorizationServer: discovery.authorizationServer,
@@ -6021,14 +6030,17 @@ function assertSecureUrl(value, label) {
6021
6030
  throw new Error(`${label} must not include credentials or fragment`);
6022
6031
  }
6023
6032
  if (url.protocol === "https:") {
6024
- return;
6033
+ return url;
6025
6034
  }
6026
6035
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
6027
- return;
6036
+ return url;
6028
6037
  }
6029
6038
  throw new Error(`${label} must use https unless it targets a loopback host`);
6030
6039
  }
6031
6040
  function assertSecureOAuthFlowEndpoints(metadata) {
6041
+ const issuer = assertSecureUrl(requireOwnString(metadata, "issuer", "Authorization server metadata"), "Authorization server issuer");
6042
+ if (issuer.href.includes("?"))
6043
+ throw new Error("Authorization server issuer must not include query or fragment");
6032
6044
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
6033
6045
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
6034
6046
  const registrationEndpoint = getOwnString2(metadata, "registration_endpoint");
@@ -7747,6 +7759,13 @@ var HttpTransport = class {
7747
7759
  }) {
7748
7760
  if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1)
7749
7761
  throw new Error("HTTP response byte limit must be a positive safe integer");
7762
+ try {
7763
+ const target = new URL(url);
7764
+ if (!["http:", "https:"].includes(target.protocol) || target.username || target.password || target.href.includes("#"))
7765
+ throw new Error("Invalid HTTP transport target");
7766
+ } catch {
7767
+ throw new Error("HTTP transport URL must be an absolute HTTP URL without credentials or fragment");
7768
+ }
7750
7769
  this.maxResponseBytes = maxResponseBytes;
7751
7770
  this.url = url;
7752
7771
  this.mode = mode;
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.350",
11
+ "version": "0.0.352",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.350",
3
+ "version": "0.0.352",
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.350",
3
+ "version": "0.0.352",
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.350",
161
+ "toolcraft-schema": "0.0.352",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",