toolcraft 0.0.350 → 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.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.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.350",
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.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.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.350",
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.80",
21
+ "version": "0.1.82",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -171,7 +171,14 @@ refresh retains the previous granted set. Mismatched responses never activate
171
171
  credentials; an unusable refresh response retains the pending refresh record.
172
172
  `resourceIdentity` selects a native-owned logical server within its optional
173
173
  persistence namespace. One encrypted, locked document owns that identity's
174
- 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
175
182
  credentials permanently; returning to the old URL does not restore them.
176
183
  Retired identities also withhold stale initial grants; authorize again or select
177
184
  a fresh explicit profile to import a new grant. This option requires native persistence; custom
@@ -194,7 +201,9 @@ using the original configured client. Persisted sessions take precedence,
194
201
  including sessions whose tokens have been cleared; an import cannot revive them.
195
202
  Input tokens are copied before any host clock anchors a relative lifetime;
196
203
  clock mutation cannot replace the selected access/refresh/scope values. Invalid
197
- 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.
198
207
  For a raw OAuth response, `parseOAuthTokenGrant(response, { issuedAt, expiresAt })`
199
208
  returns normalized `StoredOAuthTokens`. Timing options are captured before the
200
209
  host clock runs, so it cannot bypass their earlier validation. The parser accepts
@@ -172,6 +172,11 @@ export function createDefaultOAuthClientProvider(options) {
172
172
  };
173
173
  async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
174
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
+ }
175
180
  const canonicalResource = canonicalizeResourceIndicator(resource);
176
181
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
177
182
  let session = await loadSession(canonicalResource);
@@ -187,9 +192,6 @@ export function createDefaultOAuthClientProvider(options) {
187
192
  if (session !== null && initialGrant?.resource === canonicalResource)
188
193
  initialGrantConsumed = true;
189
194
  signal?.throwIfAborted();
190
- if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
191
- throw new Error("OAuth discovery authorization-server issuer mismatch");
192
- }
193
195
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
194
196
  || getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
195
197
  || (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
@@ -198,7 +200,6 @@ export function createDefaultOAuthClientProvider(options) {
198
200
  }
199
201
  if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
200
202
  initialGrant.tokens !== undefined && initialGrant.client !== null) {
201
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
202
203
  session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
203
204
  client: initialGrant.client, tokens: initialGrant.tokens,
204
205
  ...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
@@ -766,14 +767,17 @@ function assertSecureUrl(value, label) {
766
767
  throw new Error(`${label} must not include credentials or fragment`);
767
768
  }
768
769
  if (url.protocol === "https:") {
769
- return;
770
+ return url;
770
771
  }
771
772
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
772
- return;
773
+ return url;
773
774
  }
774
775
  throw new Error(`${label} must use https unless it targets a loopback host`);
775
776
  }
776
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");
777
781
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
778
782
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
779
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.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
@@ -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
  }
@@ -5462,6 +5467,11 @@ function createDefaultOAuthClientProvider(options) {
5462
5467
  };
5463
5468
  async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
5464
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
+ }
5465
5475
  const canonicalResource = canonicalizeResourceIndicator(resource);
5466
5476
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5467
5477
  let session = await loadSession(canonicalResource);
@@ -5477,15 +5487,11 @@ function createDefaultOAuthClientProvider(options) {
5477
5487
  if (session !== null && initialGrant?.resource === canonicalResource)
5478
5488
  initialGrantConsumed = true;
5479
5489
  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
5490
  if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
5484
5491
  await clearSession(canonicalResource);
5485
5492
  session = null;
5486
5493
  }
5487
5494
  if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
5488
- assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
5489
5495
  session = {
5490
5496
  resource: canonicalResource,
5491
5497
  authorizationServer: discovery.authorizationServer,
@@ -6021,14 +6027,17 @@ function assertSecureUrl(value, label) {
6021
6027
  throw new Error(`${label} must not include credentials or fragment`);
6022
6028
  }
6023
6029
  if (url.protocol === "https:") {
6024
- return;
6030
+ return url;
6025
6031
  }
6026
6032
  if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
6027
- return;
6033
+ return url;
6028
6034
  }
6029
6035
  throw new Error(`${label} must use https unless it targets a loopback host`);
6030
6036
  }
6031
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");
6032
6041
  const authorizationEndpoint = requireOwnString(metadata, "authorization_endpoint", "Authorization server metadata");
6033
6042
  const tokenEndpoint = requireOwnString(metadata, "token_endpoint", "Authorization server metadata");
6034
6043
  const registrationEndpoint = getOwnString2(metadata, "registration_endpoint");
@@ -7747,6 +7756,13 @@ var HttpTransport = class {
7747
7756
  }) {
7748
7757
  if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1)
7749
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
+ }
7750
7766
  this.maxResponseBytes = maxResponseBytes;
7751
7767
  this.url = url;
7752
7768
  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.351",
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.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.350",
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.350",
161
+ "toolcraft-schema": "0.0.351",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",