toolcraft 0.0.124 → 0.0.126
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 +8 -3
- package/dist/composition.json +8 -3
- package/dist/http.d.ts +10 -0
- package/dist/http.js +40 -0
- package/node_modules/mcp-oauth-server/LICENSE +21 -0
- package/node_modules/mcp-oauth-server/README.md +192 -0
- package/node_modules/mcp-oauth-server/dist/index.d.ts +157 -0
- package/node_modules/mcp-oauth-server/dist/index.js +641 -0
- package/node_modules/mcp-oauth-server/package.json +34 -0
- package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +6 -4
package/composition.json
CHANGED
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"version": "0.0.1",
|
|
77
77
|
"license": "MIT"
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
"name": "mcp-oauth-server",
|
|
81
|
+
"version": "0.1.0",
|
|
82
|
+
"license": "MIT"
|
|
83
|
+
},
|
|
79
84
|
{
|
|
80
85
|
"name": "sisteransi",
|
|
81
86
|
"version": "1.0.5",
|
|
@@ -88,7 +93,7 @@
|
|
|
88
93
|
},
|
|
89
94
|
{
|
|
90
95
|
"name": "tiny-http-mcp-server",
|
|
91
|
-
"version": "0.1.
|
|
96
|
+
"version": "0.1.4",
|
|
92
97
|
"license": "MIT"
|
|
93
98
|
},
|
|
94
99
|
{
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
"name": "toolcraft",
|
|
106
|
-
"version": "0.0.
|
|
111
|
+
"version": "0.0.126",
|
|
107
112
|
"license": "MIT"
|
|
108
113
|
},
|
|
109
114
|
{
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"name": "toolcraft-schema",
|
|
116
|
-
"version": "0.0.
|
|
121
|
+
"version": "0.0.126",
|
|
117
122
|
"license": "MIT"
|
|
118
123
|
},
|
|
119
124
|
{
|
package/dist/composition.json
CHANGED
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"version": "0.0.1",
|
|
77
77
|
"license": "MIT"
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
"name": "mcp-oauth-server",
|
|
81
|
+
"version": "0.1.0",
|
|
82
|
+
"license": "MIT"
|
|
83
|
+
},
|
|
79
84
|
{
|
|
80
85
|
"name": "sisteransi",
|
|
81
86
|
"version": "1.0.5",
|
|
@@ -88,7 +93,7 @@
|
|
|
88
93
|
},
|
|
89
94
|
{
|
|
90
95
|
"name": "tiny-http-mcp-server",
|
|
91
|
-
"version": "0.1.
|
|
96
|
+
"version": "0.1.4",
|
|
92
97
|
"license": "MIT"
|
|
93
98
|
},
|
|
94
99
|
{
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
"name": "toolcraft",
|
|
106
|
-
"version": "0.0.
|
|
111
|
+
"version": "0.0.126",
|
|
107
112
|
"license": "MIT"
|
|
108
113
|
},
|
|
109
114
|
{
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"name": "toolcraft-schema",
|
|
116
|
-
"version": "0.0.
|
|
121
|
+
"version": "0.0.126",
|
|
117
122
|
"license": "MIT"
|
|
118
123
|
},
|
|
119
124
|
{
|
package/dist/http.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./node-require-shim.js";
|
|
2
2
|
import { type HttpListenOptions, type HttpServer, type HttpServerHandle, type HttpToolContext, type HttpTransportOptions } from "tiny-http-mcp-server/server";
|
|
3
3
|
import type { Group } from "./index.js";
|
|
4
|
+
import type { OAuthAuthorizationServer } from "mcp-oauth-server";
|
|
4
5
|
import { type RunMCPOptions } from "./mcp.js";
|
|
5
6
|
export type ToolcraftHTTPContext = HttpToolContext;
|
|
6
7
|
export type ToolcraftHTTPServer = HttpServer;
|
|
@@ -13,7 +14,16 @@ export interface RunHTTPMCPOptions<TServices extends object = Record<string, unk
|
|
|
13
14
|
path?: string;
|
|
14
15
|
requestServices?(context: ToolcraftHTTPContext): Partial<TServices> | Promise<Partial<TServices>>;
|
|
15
16
|
}
|
|
17
|
+
export interface HTTPMCPAuthorizationOptions {
|
|
18
|
+
authorizationServer: Pick<OAuthAuthorizationServer, "issuer" | "verifyAccessToken">;
|
|
19
|
+
resource: string;
|
|
20
|
+
requiredScopes?: readonly string[];
|
|
21
|
+
scopesSupported?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function createHTTPMCPAuthorization(options: HTTPMCPAuthorizationOptions): import("tiny-http-mcp-server/server").TinyHttpMcpServerOAuthOptions;
|
|
16
24
|
export declare function createHTTPMCPServer<TServices extends object = Record<string, unknown>>(roots: Group<TServices> | Group<TServices>[], options: RunHTTPMCPOptions<TServices>): Promise<ToolcraftHTTPServer>;
|
|
17
25
|
export declare function runHTTPMCP<TServices extends object = Record<string, unknown>>(roots: Group<TServices> | Group<TServices>[], options: RunHTTPMCPOptions<TServices>): Promise<ToolcraftHTTPServerHandle>;
|
|
18
26
|
export type { HttpListenOptions, HttpObservabilityEvent, HttpObservabilityOptions, RequestAuthInfo, Session, SessionStore, StreamableHttpTransportOptions, TinyHttpMcpServerOAuthOptions, TokenVerifier, VerifiedAccessToken } from "tiny-http-mcp-server/server";
|
|
19
27
|
export { createJwksTokenVerifier, TokenVerificationError } from "tiny-http-mcp-server/server";
|
|
28
|
+
export { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
|
|
29
|
+
export type { AuthorizationInteraction, AuthorizationInteractionSecurity, AuthorizationServerStore, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, VerifiedAuthorizationServerToken } from "mcp-oauth-server";
|
package/dist/http.js
CHANGED
|
@@ -2,6 +2,45 @@ import "./node-require-shim.js";
|
|
|
2
2
|
import { createHttpServer } from "tiny-http-mcp-server/server";
|
|
3
3
|
import { createMCPServerForTransport } from "./mcp.js";
|
|
4
4
|
import { enableSourceMaps } from "./stack-trim.js";
|
|
5
|
+
function toVerifiedAccessToken(token, issuer, verified) {
|
|
6
|
+
return {
|
|
7
|
+
token,
|
|
8
|
+
issuer,
|
|
9
|
+
audience: [verified.resource],
|
|
10
|
+
scopes: [...verified.scopes],
|
|
11
|
+
expiresAt: verified.expiresAt,
|
|
12
|
+
claims: {
|
|
13
|
+
sub: verified.subject,
|
|
14
|
+
client_id: verified.clientId,
|
|
15
|
+
aud: verified.resource,
|
|
16
|
+
jti: verified.tokenId
|
|
17
|
+
},
|
|
18
|
+
subject: verified.subject,
|
|
19
|
+
clientId: verified.clientId
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createHTTPMCPAuthorization(options) {
|
|
23
|
+
const issuer = options.authorizationServer.issuer;
|
|
24
|
+
const resource = new URL(options.resource).href;
|
|
25
|
+
return {
|
|
26
|
+
resource,
|
|
27
|
+
authorizationServers: [issuer],
|
|
28
|
+
requiredScopes: options.requiredScopes,
|
|
29
|
+
scopesSupported: options.scopesSupported ?? options.requiredScopes,
|
|
30
|
+
verifier: {
|
|
31
|
+
async verify(input) {
|
|
32
|
+
if (input.authorizationServers.length !== 1 || input.authorizationServers[0] !== issuer) {
|
|
33
|
+
throw new Error("authorization server issuer does not match");
|
|
34
|
+
}
|
|
35
|
+
if (new URL(input.resource).href !== resource) {
|
|
36
|
+
throw new Error("protected resource does not match");
|
|
37
|
+
}
|
|
38
|
+
const verified = await options.authorizationServer.verifyAccessToken(input.token, resource);
|
|
39
|
+
return toVerifiedAccessToken(input.token, issuer, verified);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
5
44
|
function createTransportOptions(options, serverOptions) {
|
|
6
45
|
return {
|
|
7
46
|
...serverOptions,
|
|
@@ -60,3 +99,4 @@ export async function runHTTPMCP(roots, options) {
|
|
|
60
99
|
return server.listenHttp(createListenOptions(options));
|
|
61
100
|
}
|
|
62
101
|
export { createJwksTokenVerifier, TokenVerificationError } from "tiny-http-mcp-server/server";
|
|
102
|
+
export { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poe Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# mcp-oauth-server
|
|
2
|
+
|
|
3
|
+
Production OAuth 2.1 authorization-server primitives for MCP applications.
|
|
4
|
+
|
|
5
|
+
The package provides:
|
|
6
|
+
|
|
7
|
+
- authorization code with mandatory PKCE `S256`
|
|
8
|
+
- RFC 8414 authorization-server metadata
|
|
9
|
+
- RFC 7591 dynamic registration for public MCP clients
|
|
10
|
+
- exact registered redirect URI validation
|
|
11
|
+
- RFC 8707 resource indicators and audience-bound JWT access tokens
|
|
12
|
+
- JWKS publication and short-lived `at+jwt` access tokens
|
|
13
|
+
- rotating refresh tokens with replay-triggered family revocation
|
|
14
|
+
- token and grant revocation
|
|
15
|
+
- asynchronous durable storage interfaces
|
|
16
|
+
- an application-owned browser authorization interaction
|
|
17
|
+
- CSRF, state, nonce, and secure cookie helpers
|
|
18
|
+
- a Toolcraft adapter through `createHTTPMCPAuthorization()` from `toolcraft/http`
|
|
19
|
+
|
|
20
|
+
## Availability
|
|
21
|
+
|
|
22
|
+
This package is bundled privately inside the published `toolcraft` package and
|
|
23
|
+
is supported through the `toolcraft/http` exports. It is not published as a
|
|
24
|
+
standalone npm package because that npm package name is owned outside this
|
|
25
|
+
repository.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
31
|
+
import { exportJWK } from "jose";
|
|
32
|
+
import {
|
|
33
|
+
createHTTPMCPAuthorization,
|
|
34
|
+
createOAuthAuthorizationServer,
|
|
35
|
+
runHTTPMCP,
|
|
36
|
+
type AuthorizationInteraction,
|
|
37
|
+
type AuthorizationServerStore
|
|
38
|
+
} from "toolcraft/http";
|
|
39
|
+
|
|
40
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", {
|
|
41
|
+
namedCurve: "P-256"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const store: AuthorizationServerStore = createDurableStore();
|
|
45
|
+
const interaction: AuthorizationInteraction = {
|
|
46
|
+
async start({ transaction }) {
|
|
47
|
+
return renderApplicationAuthorizationPage(transaction);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const authorizationServer = createOAuthAuthorizationServer({
|
|
52
|
+
issuer: "https://auth.example.com",
|
|
53
|
+
resources: ["https://mcp.example.com/mcp"],
|
|
54
|
+
signingKey: {
|
|
55
|
+
algorithm: "ES256",
|
|
56
|
+
keyId: "2026-07",
|
|
57
|
+
privateKey,
|
|
58
|
+
publicJwk: await exportJWK(publicKey)
|
|
59
|
+
},
|
|
60
|
+
store,
|
|
61
|
+
interaction
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await runHTTPMCP(groups, {
|
|
65
|
+
oauth: createHTTPMCPAuthorization({
|
|
66
|
+
authorizationServer,
|
|
67
|
+
resource: "https://mcp.example.com/mcp",
|
|
68
|
+
requiredScopes: ["mcp.read"]
|
|
69
|
+
}),
|
|
70
|
+
requestServices(context) {
|
|
71
|
+
if (context.auth === undefined) throw new Error("OAuth subject required");
|
|
72
|
+
return {
|
|
73
|
+
babyDaybook: loadBabyDaybookSession(context.auth.subject)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Route requests for `/.well-known/oauth-authorization-server`,
|
|
80
|
+
`/.well-known/jwks.json`, `/register`, `/authorize`, `/token`, and `/revoke`
|
|
81
|
+
to `authorizationServer.handle(request)` in the application HTTP framework.
|
|
82
|
+
|
|
83
|
+
## Application Authorization Interaction
|
|
84
|
+
|
|
85
|
+
`interaction.start({ request, transaction })` owns the browser experience. It
|
|
86
|
+
may render a login page, redirect to an upstream identity provider, or render a
|
|
87
|
+
one-time credential completion form. The application finishes the OAuth flow
|
|
88
|
+
only after authenticating its user:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const completion = await authorizationServer.completeAuthorization({
|
|
92
|
+
transactionId,
|
|
93
|
+
subject: applicationUserId,
|
|
94
|
+
scopes: approvedScopes
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return Response.redirect(completion.redirectUrl);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For an Apple `intent://callback?...` completion flow:
|
|
101
|
+
|
|
102
|
+
1. Generate the application CSRF token, upstream state, and nonce with
|
|
103
|
+
`createAuthorizationInteractionSecurity()`.
|
|
104
|
+
2. Bind the upstream state to the pending authorization transaction in the
|
|
105
|
+
application's durable store.
|
|
106
|
+
3. Render the callback paste form with the hidden CSRF token and returned
|
|
107
|
+
`Set-Cookie` value.
|
|
108
|
+
4. On submission, bound the request body, verify CSRF with
|
|
109
|
+
`verifyAuthorizationInteractionCsrf()`, validate the exact intent callback
|
|
110
|
+
shape and state, and consume the application transaction once.
|
|
111
|
+
5. Exchange the one-time callback immediately. Never log or persist the
|
|
112
|
+
callback, password, authorization code, access token, or refresh token in
|
|
113
|
+
plaintext.
|
|
114
|
+
6. Persist only the encrypted upstream refresh session under the approved OAuth
|
|
115
|
+
subject, then call `completeAuthorization()`.
|
|
116
|
+
|
|
117
|
+
The application credential is deliberately absent from all package APIs so it
|
|
118
|
+
cannot accidentally enter OAuth transaction or token storage.
|
|
119
|
+
|
|
120
|
+
## Durable Storage
|
|
121
|
+
|
|
122
|
+
Production deployments must implement `AuthorizationServerStore`. The
|
|
123
|
+
interface covers:
|
|
124
|
+
|
|
125
|
+
- dynamic clients
|
|
126
|
+
- browser authorization transactions
|
|
127
|
+
- authorization grants
|
|
128
|
+
- one-time authorization codes
|
|
129
|
+
- access-token revocation records
|
|
130
|
+
- refresh-token families
|
|
131
|
+
|
|
132
|
+
`takeAuthorizationTransaction()`, `takeAuthorizationCode()`, and
|
|
133
|
+
`rotateRefreshToken()` must be atomic. Refresh replay detection depends on the
|
|
134
|
+
store retaining rotated token-family relationships and revoking the family
|
|
135
|
+
when an old token is presented again.
|
|
136
|
+
|
|
137
|
+
`createInMemoryAuthorizationServerStore()` is intended for tests and local
|
|
138
|
+
development only. It is not durable and must not be used in production.
|
|
139
|
+
|
|
140
|
+
## Subject Isolation
|
|
141
|
+
|
|
142
|
+
The access token `sub` claim is the only identity passed to Toolcraft as
|
|
143
|
+
`context.auth.subject`. Application service lookup must require that subject
|
|
144
|
+
and load only credentials stored under the same subject. Do not fall back to a
|
|
145
|
+
shared environment credential when a subject session is missing.
|
|
146
|
+
|
|
147
|
+
Grant and token revocation use opaque identifiers and token hashes. Raw
|
|
148
|
+
authorization codes, access tokens, and refresh tokens are never stored by the
|
|
149
|
+
provided store contract.
|
|
150
|
+
|
|
151
|
+
## Security Requirements
|
|
152
|
+
|
|
153
|
+
- Terminate TLS before exposing any endpoint publicly.
|
|
154
|
+
- Keep signing private keys outside source control and rotate keys with an
|
|
155
|
+
overlap period long enough for issued access tokens to expire.
|
|
156
|
+
- Use a durable store implementation with transactional or compare-and-swap
|
|
157
|
+
semantics for one-time consumption and refresh rotation.
|
|
158
|
+
- Keep access-token lifetimes short. The default is five minutes.
|
|
159
|
+
- Set restrictive CSP, `Referrer-Policy: no-referrer`, and `Cache-Control:
|
|
160
|
+
no-store` on application authorization pages.
|
|
161
|
+
- Require exact redirect URI matches. Do not add wildcard or prefix matching.
|
|
162
|
+
- Treat every OAuth subject as a separate security boundary.
|
|
163
|
+
- Encrypt application-owned upstream refresh tokens at rest with a
|
|
164
|
+
production key-management service.
|
|
165
|
+
- Redact authorization headers, cookies, codes, callbacks, and tokens from
|
|
166
|
+
logs and traces.
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
`createOAuthAuthorizationServer(options)` accepts:
|
|
171
|
+
|
|
172
|
+
| Option | Required | Default | Description |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| `issuer` | yes | none | HTTPS authorization-server issuer. Loopback HTTP is allowed for local development. |
|
|
175
|
+
| `resources` | yes | none | Exact protected resource identifiers accepted through the OAuth `resource` parameter. |
|
|
176
|
+
| `signingKey` | yes | none | Current `ES256` or `RS256` private key, public JWK, and key id. |
|
|
177
|
+
| `additionalPublicJwks` | no | `[]` | Previous public signing keys retained in JWKS during a key-rotation overlap. |
|
|
178
|
+
| `store` | yes | none | Durable `AuthorizationServerStore` implementation. |
|
|
179
|
+
| `interaction` | yes | none | Application-owned browser authorization hook. |
|
|
180
|
+
| `accessTokenTtlSeconds` | no | `300` | Signed access-token lifetime. |
|
|
181
|
+
| `authorizationCodeTtlSeconds` | no | `60` | One-time authorization-code lifetime. |
|
|
182
|
+
| `authorizationTransactionTtlSeconds` | no | `600` | Pending browser interaction lifetime. |
|
|
183
|
+
| `refreshTokenTtlSeconds` | no | `2592000` | Rotating refresh-token lifetime. |
|
|
184
|
+
| `maxRequestBodyBytes` | no | `65536` | Maximum DCR, token, and revocation request body size. |
|
|
185
|
+
| `now` | no | `Date.now` | Clock override for tests. |
|
|
186
|
+
| `randomToken` | no | cryptographic random | Opaque identifier generator override for tests. |
|
|
187
|
+
|
|
188
|
+
## Environment Variables
|
|
189
|
+
|
|
190
|
+
This package reads no environment variables. Signing keys, durable database
|
|
191
|
+
connections, encryption keys, issuer URLs, and protected resource identifiers
|
|
192
|
+
must be passed explicitly by the application.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type JWK } from "jose";
|
|
3
|
+
export interface OAuthClientRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
redirectUris: readonly string[];
|
|
6
|
+
createdAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthorizationTransactionRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
codeChallenge: string;
|
|
13
|
+
resource: string;
|
|
14
|
+
scopes: readonly string[];
|
|
15
|
+
state?: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface AuthorizationCodeRecord {
|
|
20
|
+
tokenHash: string;
|
|
21
|
+
grantId: string;
|
|
22
|
+
clientId: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
redirectUri: string;
|
|
25
|
+
codeChallenge: string;
|
|
26
|
+
resource: string;
|
|
27
|
+
scopes: readonly string[];
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
export interface AuthorizationGrantRecord {
|
|
31
|
+
id: string;
|
|
32
|
+
clientId: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
resource: string;
|
|
35
|
+
scopes: readonly string[];
|
|
36
|
+
createdAt: number;
|
|
37
|
+
revokedAt?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RefreshTokenRecord {
|
|
40
|
+
tokenHash: string;
|
|
41
|
+
familyId: string;
|
|
42
|
+
grantId: string;
|
|
43
|
+
clientId: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
resource: string;
|
|
46
|
+
scopes: readonly string[];
|
|
47
|
+
createdAt: number;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
status: "active" | "rotated" | "revoked";
|
|
50
|
+
}
|
|
51
|
+
export interface AccessTokenRecord {
|
|
52
|
+
tokenHash: string;
|
|
53
|
+
tokenId: string;
|
|
54
|
+
grantId: string;
|
|
55
|
+
subject: string;
|
|
56
|
+
clientId: string;
|
|
57
|
+
resource: string;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
revokedAt?: number;
|
|
60
|
+
}
|
|
61
|
+
export type RefreshTokenRotationResult = {
|
|
62
|
+
status: "rotated";
|
|
63
|
+
previous: RefreshTokenRecord;
|
|
64
|
+
} | {
|
|
65
|
+
status: "replay";
|
|
66
|
+
} | {
|
|
67
|
+
status: "invalid";
|
|
68
|
+
};
|
|
69
|
+
export interface AuthorizationServerStore {
|
|
70
|
+
putClient(client: OAuthClientRecord): Promise<void>;
|
|
71
|
+
getClient(clientId: string): Promise<OAuthClientRecord | undefined>;
|
|
72
|
+
putAuthorizationTransaction(transaction: AuthorizationTransactionRecord): Promise<void>;
|
|
73
|
+
takeAuthorizationTransaction(transactionId: string): Promise<AuthorizationTransactionRecord | undefined>;
|
|
74
|
+
putAuthorizationCode(code: AuthorizationCodeRecord): Promise<void>;
|
|
75
|
+
takeAuthorizationCode(tokenHash: string): Promise<AuthorizationCodeRecord | undefined>;
|
|
76
|
+
putGrant(grant: AuthorizationGrantRecord): Promise<void>;
|
|
77
|
+
getGrant(grantId: string): Promise<AuthorizationGrantRecord | undefined>;
|
|
78
|
+
putAccessToken(token: AccessTokenRecord): Promise<void>;
|
|
79
|
+
getAccessToken(tokenHash: string): Promise<AccessTokenRecord | undefined>;
|
|
80
|
+
putRefreshToken(token: RefreshTokenRecord): Promise<void>;
|
|
81
|
+
rotateRefreshToken(tokenHash: string, replacementTokenHash: string, now: number, expiresAt: number): Promise<RefreshTokenRotationResult>;
|
|
82
|
+
revokeToken(tokenHash: string, now: number): Promise<void>;
|
|
83
|
+
revokeGrant(grantId: string, now: number): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export interface AuthorizationInteractionStartContext {
|
|
86
|
+
request: Request;
|
|
87
|
+
transaction: AuthorizationTransactionRecord;
|
|
88
|
+
}
|
|
89
|
+
export interface AuthorizationInteraction {
|
|
90
|
+
start(context: AuthorizationInteractionStartContext): Promise<Response> | Response;
|
|
91
|
+
}
|
|
92
|
+
export interface OAuthAuthorizationServerSigningKey {
|
|
93
|
+
algorithm: "ES256" | "RS256";
|
|
94
|
+
keyId: string;
|
|
95
|
+
privateKey: KeyObject;
|
|
96
|
+
publicJwk: JWK;
|
|
97
|
+
}
|
|
98
|
+
export interface OAuthAuthorizationServerOptions {
|
|
99
|
+
issuer: string;
|
|
100
|
+
resources: readonly string[];
|
|
101
|
+
signingKey: OAuthAuthorizationServerSigningKey;
|
|
102
|
+
additionalPublicJwks?: readonly JWK[];
|
|
103
|
+
store: AuthorizationServerStore;
|
|
104
|
+
interaction: AuthorizationInteraction;
|
|
105
|
+
accessTokenTtlSeconds?: number;
|
|
106
|
+
authorizationCodeTtlSeconds?: number;
|
|
107
|
+
authorizationTransactionTtlSeconds?: number;
|
|
108
|
+
refreshTokenTtlSeconds?: number;
|
|
109
|
+
maxRequestBodyBytes?: number;
|
|
110
|
+
now?: () => number;
|
|
111
|
+
randomToken?: () => string;
|
|
112
|
+
}
|
|
113
|
+
export interface CompleteAuthorizationInput {
|
|
114
|
+
transactionId: string;
|
|
115
|
+
subject: string;
|
|
116
|
+
scopes?: readonly string[];
|
|
117
|
+
}
|
|
118
|
+
export interface CompleteAuthorizationResult {
|
|
119
|
+
redirectUrl: URL;
|
|
120
|
+
grantId: string;
|
|
121
|
+
}
|
|
122
|
+
export interface AuthorizationInteractionSecurity {
|
|
123
|
+
csrfToken: string;
|
|
124
|
+
state: string;
|
|
125
|
+
nonce: string;
|
|
126
|
+
setCookie: string;
|
|
127
|
+
}
|
|
128
|
+
export interface AuthorizationInteractionSecurityOptions {
|
|
129
|
+
cookieName?: string;
|
|
130
|
+
maxAgeSeconds?: number;
|
|
131
|
+
randomToken?: () => string;
|
|
132
|
+
}
|
|
133
|
+
export interface VerifyAuthorizationInteractionCsrfInput {
|
|
134
|
+
cookieHeader: string | null;
|
|
135
|
+
submittedToken: string;
|
|
136
|
+
cookieName?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface OAuthAuthorizationServer {
|
|
139
|
+
issuer: string;
|
|
140
|
+
handle(request: Request): Promise<Response>;
|
|
141
|
+
completeAuthorization(input: CompleteAuthorizationInput): Promise<CompleteAuthorizationResult>;
|
|
142
|
+
denyAuthorization(transactionId: string, error?: string): Promise<URL>;
|
|
143
|
+
revokeGrant(grantId: string): Promise<void>;
|
|
144
|
+
verifyAccessToken(token: string, resource: string): Promise<VerifiedAuthorizationServerToken>;
|
|
145
|
+
}
|
|
146
|
+
export interface VerifiedAuthorizationServerToken {
|
|
147
|
+
subject: string;
|
|
148
|
+
clientId: string;
|
|
149
|
+
resource: string;
|
|
150
|
+
scopes: readonly string[];
|
|
151
|
+
tokenId: string;
|
|
152
|
+
expiresAt: number;
|
|
153
|
+
}
|
|
154
|
+
export declare function createAuthorizationInteractionSecurity(options?: AuthorizationInteractionSecurityOptions): AuthorizationInteractionSecurity;
|
|
155
|
+
export declare function verifyAuthorizationInteractionCsrf(input: VerifyAuthorizationInteractionCsrfInput): boolean;
|
|
156
|
+
export declare function createInMemoryAuthorizationServerStore(): AuthorizationServerStore;
|
|
157
|
+
export declare function createOAuthAuthorizationServer(options: OAuthAuthorizationServerOptions): OAuthAuthorizationServer;
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { importJWK, jwtVerify, SignJWT } from "jose";
|
|
3
|
+
class OAuthProtocolError extends Error {
|
|
4
|
+
error;
|
|
5
|
+
status;
|
|
6
|
+
constructor(error, message, status = 400) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.error = error;
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function opaqueToken() {
|
|
13
|
+
return randomBytes(32).toString("base64url");
|
|
14
|
+
}
|
|
15
|
+
function hashToken(token) {
|
|
16
|
+
return createHash("sha256").update(token).digest("base64url");
|
|
17
|
+
}
|
|
18
|
+
export function createAuthorizationInteractionSecurity(options = {}) {
|
|
19
|
+
const randomToken = options.randomToken ?? opaqueToken;
|
|
20
|
+
const cookieName = options.cookieName ?? "__Host-mcp_oauth_csrf";
|
|
21
|
+
const maxAgeSeconds = options.maxAgeSeconds ?? 600;
|
|
22
|
+
if (!cookieName.startsWith("__Host-") || cookieName.includes(";") || cookieName.includes("=")) {
|
|
23
|
+
throw new Error("CSRF cookie name must use the __Host- prefix.");
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
|
|
26
|
+
throw new Error("CSRF cookie max age must be a positive integer.");
|
|
27
|
+
}
|
|
28
|
+
const csrfToken = randomToken();
|
|
29
|
+
return {
|
|
30
|
+
csrfToken,
|
|
31
|
+
state: randomToken(),
|
|
32
|
+
nonce: randomToken(),
|
|
33
|
+
setCookie: `${cookieName}=${csrfToken}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly; Secure; SameSite=Lax`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function verifyAuthorizationInteractionCsrf(input) {
|
|
37
|
+
const cookieName = input.cookieName ?? "__Host-mcp_oauth_csrf";
|
|
38
|
+
const cookieValue = input.cookieHeader
|
|
39
|
+
?.split(";")
|
|
40
|
+
.map((entry) => entry.trim())
|
|
41
|
+
.find((entry) => entry.startsWith(`${cookieName}=`))
|
|
42
|
+
?.slice(cookieName.length + 1);
|
|
43
|
+
if (cookieValue === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
const cookieBuffer = Buffer.from(cookieValue);
|
|
46
|
+
const submittedBuffer = Buffer.from(input.submittedToken);
|
|
47
|
+
return (cookieBuffer.length === submittedBuffer.length &&
|
|
48
|
+
timingSafeEqual(cookieBuffer, submittedBuffer));
|
|
49
|
+
}
|
|
50
|
+
function isObject(value) {
|
|
51
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function exactStringArray(value) {
|
|
54
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
55
|
+
? value
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
function parseAbsoluteUrl(value, label) {
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new OAuthProtocolError("invalid_request", `${label} must be an absolute URL.`);
|
|
65
|
+
}
|
|
66
|
+
if (url.hash.length > 0) {
|
|
67
|
+
throw new OAuthProtocolError("invalid_request", `${label} must not contain a fragment.`);
|
|
68
|
+
}
|
|
69
|
+
return url.href;
|
|
70
|
+
}
|
|
71
|
+
function validateIssuer(value) {
|
|
72
|
+
const issuer = new URL(parseAbsoluteUrl(value, "issuer"));
|
|
73
|
+
if (issuer.protocol !== "https:" && issuer.hostname !== "localhost" && issuer.hostname !== "127.0.0.1") {
|
|
74
|
+
throw new Error("issuer must use HTTPS unless it is loopback.");
|
|
75
|
+
}
|
|
76
|
+
if (issuer.pathname !== "/" || issuer.search.length > 0) {
|
|
77
|
+
throw new Error("issuer must be an origin URL without a path or query.");
|
|
78
|
+
}
|
|
79
|
+
return issuer.href.replace(/\/$/, "");
|
|
80
|
+
}
|
|
81
|
+
function validateResources(values) {
|
|
82
|
+
if (values.length === 0) {
|
|
83
|
+
throw new Error("At least one protected resource is required.");
|
|
84
|
+
}
|
|
85
|
+
return new Set(values.map((value) => parseAbsoluteUrl(value, "resource")));
|
|
86
|
+
}
|
|
87
|
+
function parseScopes(value) {
|
|
88
|
+
if (value === null || value.length === 0)
|
|
89
|
+
return [];
|
|
90
|
+
const scopes = value.split(" ");
|
|
91
|
+
if (scopes.some((scope) => scope.length === 0 || /[\u0000-\u0020\u007f]/u.test(scope))) {
|
|
92
|
+
throw new OAuthProtocolError("invalid_scope", "scope contains an invalid value.");
|
|
93
|
+
}
|
|
94
|
+
return [...new Set(scopes)];
|
|
95
|
+
}
|
|
96
|
+
function formResponse(payload, status = 200) {
|
|
97
|
+
return Response.json(payload, {
|
|
98
|
+
status,
|
|
99
|
+
headers: {
|
|
100
|
+
"cache-control": "no-store",
|
|
101
|
+
pragma: "no-cache"
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function protocolErrorResponse(error) {
|
|
106
|
+
if (error instanceof OAuthProtocolError) {
|
|
107
|
+
return formResponse({ error: error.error, error_description: error.message }, error.status);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
function requireFormContentType(request) {
|
|
112
|
+
const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim();
|
|
113
|
+
if (contentType !== "application/x-www-form-urlencoded") {
|
|
114
|
+
throw new OAuthProtocolError("invalid_request", "Expected form-encoded request body.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function requireJsonContentType(request) {
|
|
118
|
+
const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim();
|
|
119
|
+
if (contentType !== "application/json") {
|
|
120
|
+
throw new OAuthProtocolError("invalid_request", "Expected JSON request body.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function pkceMatches(verifier, challenge) {
|
|
124
|
+
if (verifier.length < 43 || verifier.length > 128 || !/^[A-Za-z0-9._~-]+$/u.test(verifier)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const actual = createHash("sha256").update(verifier).digest();
|
|
128
|
+
const expected = Buffer.from(challenge, "base64url");
|
|
129
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
130
|
+
}
|
|
131
|
+
function narrowedScopes(requested, approved) {
|
|
132
|
+
if (approved === undefined)
|
|
133
|
+
return [...requested];
|
|
134
|
+
const requestedSet = new Set(requested);
|
|
135
|
+
if (approved.some((scope) => !requestedSet.has(scope))) {
|
|
136
|
+
throw new Error("Approved scopes must be a subset of requested scopes.");
|
|
137
|
+
}
|
|
138
|
+
return [...new Set(approved)];
|
|
139
|
+
}
|
|
140
|
+
export function createInMemoryAuthorizationServerStore() {
|
|
141
|
+
const clients = new Map();
|
|
142
|
+
const transactions = new Map();
|
|
143
|
+
const codes = new Map();
|
|
144
|
+
const grants = new Map();
|
|
145
|
+
const accessTokens = new Map();
|
|
146
|
+
const refreshTokens = new Map();
|
|
147
|
+
function revokeFamily(familyId, now) {
|
|
148
|
+
for (const [tokenHash, token] of refreshTokens) {
|
|
149
|
+
if (token.familyId === familyId) {
|
|
150
|
+
refreshTokens.set(tokenHash, { ...token, status: "revoked" });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [grantId, grant] of grants) {
|
|
154
|
+
if (grant.revokedAt === undefined &&
|
|
155
|
+
[...refreshTokens.values()].some((token) => token.familyId === familyId && token.grantId === grantId)) {
|
|
156
|
+
grants.set(grantId, { ...grant, revokedAt: now });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
async putClient(client) {
|
|
162
|
+
clients.set(client.id, structuredClone(client));
|
|
163
|
+
},
|
|
164
|
+
async getClient(clientId) {
|
|
165
|
+
const client = clients.get(clientId);
|
|
166
|
+
return client === undefined ? undefined : structuredClone(client);
|
|
167
|
+
},
|
|
168
|
+
async putAuthorizationTransaction(transaction) {
|
|
169
|
+
transactions.set(transaction.id, structuredClone(transaction));
|
|
170
|
+
},
|
|
171
|
+
async takeAuthorizationTransaction(transactionId) {
|
|
172
|
+
const transaction = transactions.get(transactionId);
|
|
173
|
+
transactions.delete(transactionId);
|
|
174
|
+
return transaction === undefined ? undefined : structuredClone(transaction);
|
|
175
|
+
},
|
|
176
|
+
async putAuthorizationCode(code) {
|
|
177
|
+
codes.set(code.tokenHash, structuredClone(code));
|
|
178
|
+
},
|
|
179
|
+
async takeAuthorizationCode(tokenHash) {
|
|
180
|
+
const code = codes.get(tokenHash);
|
|
181
|
+
codes.delete(tokenHash);
|
|
182
|
+
return code === undefined ? undefined : structuredClone(code);
|
|
183
|
+
},
|
|
184
|
+
async putGrant(grant) {
|
|
185
|
+
grants.set(grant.id, structuredClone(grant));
|
|
186
|
+
},
|
|
187
|
+
async getGrant(grantId) {
|
|
188
|
+
const grant = grants.get(grantId);
|
|
189
|
+
return grant === undefined ? undefined : structuredClone(grant);
|
|
190
|
+
},
|
|
191
|
+
async putAccessToken(token) {
|
|
192
|
+
accessTokens.set(token.tokenHash, structuredClone(token));
|
|
193
|
+
},
|
|
194
|
+
async getAccessToken(tokenHash) {
|
|
195
|
+
const token = accessTokens.get(tokenHash);
|
|
196
|
+
return token === undefined ? undefined : structuredClone(token);
|
|
197
|
+
},
|
|
198
|
+
async putRefreshToken(token) {
|
|
199
|
+
refreshTokens.set(token.tokenHash, structuredClone(token));
|
|
200
|
+
},
|
|
201
|
+
async rotateRefreshToken(tokenHash, replacementTokenHash, now, expiresAt) {
|
|
202
|
+
const token = refreshTokens.get(tokenHash);
|
|
203
|
+
if (token === undefined || token.expiresAt <= now || token.status === "revoked") {
|
|
204
|
+
return { status: "invalid" };
|
|
205
|
+
}
|
|
206
|
+
if (token.status === "rotated") {
|
|
207
|
+
revokeFamily(token.familyId, now);
|
|
208
|
+
return { status: "replay" };
|
|
209
|
+
}
|
|
210
|
+
refreshTokens.set(tokenHash, { ...token, status: "rotated" });
|
|
211
|
+
refreshTokens.set(replacementTokenHash, {
|
|
212
|
+
...token,
|
|
213
|
+
tokenHash: replacementTokenHash,
|
|
214
|
+
createdAt: now,
|
|
215
|
+
expiresAt,
|
|
216
|
+
status: "active"
|
|
217
|
+
});
|
|
218
|
+
return { status: "rotated", previous: structuredClone(token) };
|
|
219
|
+
},
|
|
220
|
+
async revokeToken(tokenHash, now) {
|
|
221
|
+
const refreshToken = refreshTokens.get(tokenHash);
|
|
222
|
+
if (refreshToken !== undefined) {
|
|
223
|
+
revokeFamily(refreshToken.familyId, now);
|
|
224
|
+
}
|
|
225
|
+
const accessToken = accessTokens.get(tokenHash);
|
|
226
|
+
if (accessToken !== undefined) {
|
|
227
|
+
accessTokens.set(tokenHash, { ...accessToken, revokedAt: now });
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async revokeGrant(grantId, now) {
|
|
231
|
+
const grant = grants.get(grantId);
|
|
232
|
+
if (grant !== undefined)
|
|
233
|
+
grants.set(grantId, { ...grant, revokedAt: now });
|
|
234
|
+
for (const [tokenHash, token] of refreshTokens) {
|
|
235
|
+
if (token.grantId === grantId) {
|
|
236
|
+
refreshTokens.set(tokenHash, { ...token, status: "revoked" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const [tokenHash, token] of accessTokens) {
|
|
240
|
+
if (token.grantId === grantId) {
|
|
241
|
+
accessTokens.set(tokenHash, { ...token, revokedAt: now });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export function createOAuthAuthorizationServer(options) {
|
|
248
|
+
const issuer = validateIssuer(options.issuer);
|
|
249
|
+
const resources = validateResources(options.resources);
|
|
250
|
+
const now = options.now ?? Date.now;
|
|
251
|
+
const randomToken = options.randomToken ?? opaqueToken;
|
|
252
|
+
const accessTokenTtlMs = (options.accessTokenTtlSeconds ?? 300) * 1000;
|
|
253
|
+
const authorizationCodeTtlMs = (options.authorizationCodeTtlSeconds ?? 60) * 1000;
|
|
254
|
+
const authorizationTransactionTtlMs = (options.authorizationTransactionTtlSeconds ?? 600) * 1000;
|
|
255
|
+
const refreshTokenTtlMs = (options.refreshTokenTtlSeconds ?? 2_592_000) * 1000;
|
|
256
|
+
const maxRequestBodyBytes = options.maxRequestBodyBytes ?? 65_536;
|
|
257
|
+
if (!Number.isInteger(maxRequestBodyBytes) || maxRequestBodyBytes <= 0) {
|
|
258
|
+
throw new Error("maxRequestBodyBytes must be a positive integer.");
|
|
259
|
+
}
|
|
260
|
+
const publicJwk = {
|
|
261
|
+
...options.signingKey.publicJwk,
|
|
262
|
+
kid: options.signingKey.keyId,
|
|
263
|
+
alg: options.signingKey.algorithm,
|
|
264
|
+
use: "sig"
|
|
265
|
+
};
|
|
266
|
+
const publishedJwks = [publicJwk, ...(options.additionalPublicJwks ?? [])];
|
|
267
|
+
const verificationKey = importJWK(publicJwk, options.signingKey.algorithm);
|
|
268
|
+
function endpoint(path) {
|
|
269
|
+
return `${issuer}${path}`;
|
|
270
|
+
}
|
|
271
|
+
function requireResource(value) {
|
|
272
|
+
if (value === null) {
|
|
273
|
+
throw new OAuthProtocolError("invalid_target", "resource is required.");
|
|
274
|
+
}
|
|
275
|
+
const normalized = parseAbsoluteUrl(value, "resource");
|
|
276
|
+
if (!resources.has(normalized)) {
|
|
277
|
+
throw new OAuthProtocolError("invalid_target", "resource is not supported.");
|
|
278
|
+
}
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
async function readRequestBody(request) {
|
|
282
|
+
const declaredLength = request.headers.get("content-length");
|
|
283
|
+
if (declaredLength !== null) {
|
|
284
|
+
const parsedLength = Number(declaredLength);
|
|
285
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxRequestBodyBytes) {
|
|
286
|
+
throw new OAuthProtocolError("invalid_request", "Request body is too large.", 413);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
290
|
+
if (body.byteLength > maxRequestBodyBytes) {
|
|
291
|
+
throw new OAuthProtocolError("invalid_request", "Request body is too large.", 413);
|
|
292
|
+
}
|
|
293
|
+
return new TextDecoder().decode(body);
|
|
294
|
+
}
|
|
295
|
+
async function handleRegister(request) {
|
|
296
|
+
requireJsonContentType(request);
|
|
297
|
+
const body = await readRequestBody(request);
|
|
298
|
+
let payload;
|
|
299
|
+
try {
|
|
300
|
+
payload = JSON.parse(body);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
throw new OAuthProtocolError("invalid_client_metadata", "Registration body must be JSON.");
|
|
304
|
+
}
|
|
305
|
+
if (!isObject(payload)) {
|
|
306
|
+
throw new OAuthProtocolError("invalid_client_metadata", "Registration body must be an object.");
|
|
307
|
+
}
|
|
308
|
+
const redirectUris = exactStringArray(payload.redirect_uris);
|
|
309
|
+
if (redirectUris === undefined || redirectUris.length === 0) {
|
|
310
|
+
throw new OAuthProtocolError("invalid_redirect_uri", "redirect_uris is required.");
|
|
311
|
+
}
|
|
312
|
+
const normalizedRedirectUris = redirectUris.map((value) => parseAbsoluteUrl(value, "redirect_uri"));
|
|
313
|
+
if (payload.token_endpoint_auth_method !== undefined &&
|
|
314
|
+
payload.token_endpoint_auth_method !== "none") {
|
|
315
|
+
throw new OAuthProtocolError("invalid_client_metadata", "Only public clients using token_endpoint_auth_method none are supported.");
|
|
316
|
+
}
|
|
317
|
+
const grantTypes = exactStringArray(payload.grant_types) ?? ["authorization_code"];
|
|
318
|
+
if (grantTypes.some((value) => value !== "authorization_code" && value !== "refresh_token")) {
|
|
319
|
+
throw new OAuthProtocolError("invalid_client_metadata", "Unsupported grant type.");
|
|
320
|
+
}
|
|
321
|
+
const responseTypes = exactStringArray(payload.response_types) ?? ["code"];
|
|
322
|
+
if (responseTypes.length !== 1 || responseTypes[0] !== "code") {
|
|
323
|
+
throw new OAuthProtocolError("invalid_client_metadata", "Only response_type code is supported.");
|
|
324
|
+
}
|
|
325
|
+
const client = {
|
|
326
|
+
id: randomToken(),
|
|
327
|
+
redirectUris: [...new Set(normalizedRedirectUris)],
|
|
328
|
+
createdAt: now()
|
|
329
|
+
};
|
|
330
|
+
await options.store.putClient(client);
|
|
331
|
+
return formResponse({
|
|
332
|
+
client_id: client.id,
|
|
333
|
+
client_id_issued_at: Math.floor(client.createdAt / 1000),
|
|
334
|
+
redirect_uris: client.redirectUris,
|
|
335
|
+
token_endpoint_auth_method: "none",
|
|
336
|
+
grant_types: grantTypes,
|
|
337
|
+
response_types: ["code"]
|
|
338
|
+
}, 201);
|
|
339
|
+
}
|
|
340
|
+
async function handleAuthorize(request) {
|
|
341
|
+
const url = new URL(request.url);
|
|
342
|
+
if (url.searchParams.get("response_type") !== "code") {
|
|
343
|
+
throw new OAuthProtocolError("unsupported_response_type", "response_type must be code.");
|
|
344
|
+
}
|
|
345
|
+
const clientId = url.searchParams.get("client_id");
|
|
346
|
+
if (clientId === null)
|
|
347
|
+
throw new OAuthProtocolError("invalid_request", "client_id is required.");
|
|
348
|
+
const client = await options.store.getClient(clientId);
|
|
349
|
+
if (client === undefined)
|
|
350
|
+
throw new OAuthProtocolError("unauthorized_client", "Unknown client.");
|
|
351
|
+
const redirectUriValue = url.searchParams.get("redirect_uri");
|
|
352
|
+
if (redirectUriValue === null) {
|
|
353
|
+
throw new OAuthProtocolError("invalid_request", "redirect_uri is required.");
|
|
354
|
+
}
|
|
355
|
+
const redirectUri = parseAbsoluteUrl(redirectUriValue, "redirect_uri");
|
|
356
|
+
if (!client.redirectUris.includes(redirectUri)) {
|
|
357
|
+
throw new OAuthProtocolError("invalid_request", "redirect_uri is not registered.");
|
|
358
|
+
}
|
|
359
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
360
|
+
if (codeChallenge === null || !/^[A-Za-z0-9_-]{43}$/u.test(codeChallenge)) {
|
|
361
|
+
throw new OAuthProtocolError("invalid_request", "A valid code_challenge is required.");
|
|
362
|
+
}
|
|
363
|
+
if (url.searchParams.get("code_challenge_method") !== "S256") {
|
|
364
|
+
throw new OAuthProtocolError("invalid_request", "code_challenge_method must be S256.");
|
|
365
|
+
}
|
|
366
|
+
const transaction = {
|
|
367
|
+
id: randomToken(),
|
|
368
|
+
clientId,
|
|
369
|
+
redirectUri,
|
|
370
|
+
codeChallenge,
|
|
371
|
+
resource: requireResource(url.searchParams.get("resource")),
|
|
372
|
+
scopes: parseScopes(url.searchParams.get("scope")),
|
|
373
|
+
...(url.searchParams.has("state") && { state: url.searchParams.get("state") ?? undefined }),
|
|
374
|
+
createdAt: now(),
|
|
375
|
+
expiresAt: now() + authorizationTransactionTtlMs
|
|
376
|
+
};
|
|
377
|
+
await options.store.putAuthorizationTransaction(transaction);
|
|
378
|
+
return options.interaction.start({ request, transaction });
|
|
379
|
+
}
|
|
380
|
+
async function completeAuthorization(input) {
|
|
381
|
+
if (input.subject.length === 0)
|
|
382
|
+
throw new Error("subject is required.");
|
|
383
|
+
const transaction = await options.store.takeAuthorizationTransaction(input.transactionId);
|
|
384
|
+
const currentTime = now();
|
|
385
|
+
if (transaction === undefined || transaction.expiresAt <= currentTime) {
|
|
386
|
+
throw new Error("Authorization transaction is missing, expired, or already completed.");
|
|
387
|
+
}
|
|
388
|
+
const scopes = narrowedScopes(transaction.scopes, input.scopes);
|
|
389
|
+
const grantId = randomToken();
|
|
390
|
+
await options.store.putGrant({
|
|
391
|
+
id: grantId,
|
|
392
|
+
clientId: transaction.clientId,
|
|
393
|
+
subject: input.subject,
|
|
394
|
+
resource: transaction.resource,
|
|
395
|
+
scopes,
|
|
396
|
+
createdAt: currentTime
|
|
397
|
+
});
|
|
398
|
+
const code = randomToken();
|
|
399
|
+
await options.store.putAuthorizationCode({
|
|
400
|
+
tokenHash: hashToken(code),
|
|
401
|
+
grantId,
|
|
402
|
+
clientId: transaction.clientId,
|
|
403
|
+
subject: input.subject,
|
|
404
|
+
redirectUri: transaction.redirectUri,
|
|
405
|
+
codeChallenge: transaction.codeChallenge,
|
|
406
|
+
resource: transaction.resource,
|
|
407
|
+
scopes,
|
|
408
|
+
expiresAt: currentTime + authorizationCodeTtlMs
|
|
409
|
+
});
|
|
410
|
+
const redirectUrl = new URL(transaction.redirectUri);
|
|
411
|
+
redirectUrl.searchParams.set("code", code);
|
|
412
|
+
if (transaction.state !== undefined)
|
|
413
|
+
redirectUrl.searchParams.set("state", transaction.state);
|
|
414
|
+
redirectUrl.searchParams.set("iss", issuer);
|
|
415
|
+
return { redirectUrl, grantId };
|
|
416
|
+
}
|
|
417
|
+
async function denyAuthorization(transactionId, error = "access_denied") {
|
|
418
|
+
const transaction = await options.store.takeAuthorizationTransaction(transactionId);
|
|
419
|
+
if (transaction === undefined) {
|
|
420
|
+
throw new Error("Authorization transaction is missing or already completed.");
|
|
421
|
+
}
|
|
422
|
+
const redirectUrl = new URL(transaction.redirectUri);
|
|
423
|
+
redirectUrl.searchParams.set("error", error);
|
|
424
|
+
if (transaction.state !== undefined)
|
|
425
|
+
redirectUrl.searchParams.set("state", transaction.state);
|
|
426
|
+
redirectUrl.searchParams.set("iss", issuer);
|
|
427
|
+
return redirectUrl;
|
|
428
|
+
}
|
|
429
|
+
async function issueToken(input) {
|
|
430
|
+
const currentTime = now();
|
|
431
|
+
const expiresAt = currentTime + accessTokenTtlMs;
|
|
432
|
+
const tokenId = randomToken();
|
|
433
|
+
const accessToken = await new SignJWT({
|
|
434
|
+
scope: input.grant.scopes.join(" "),
|
|
435
|
+
client_id: input.grant.clientId
|
|
436
|
+
})
|
|
437
|
+
.setProtectedHeader({
|
|
438
|
+
alg: options.signingKey.algorithm,
|
|
439
|
+
kid: options.signingKey.keyId,
|
|
440
|
+
typ: "at+jwt"
|
|
441
|
+
})
|
|
442
|
+
.setIssuer(issuer)
|
|
443
|
+
.setSubject(input.grant.subject)
|
|
444
|
+
.setAudience(input.grant.resource)
|
|
445
|
+
.setJti(tokenId)
|
|
446
|
+
.setIssuedAt(Math.floor(currentTime / 1000))
|
|
447
|
+
.setExpirationTime(Math.floor(expiresAt / 1000))
|
|
448
|
+
.sign(options.signingKey.privateKey);
|
|
449
|
+
await options.store.putAccessToken({
|
|
450
|
+
tokenHash: hashToken(accessToken),
|
|
451
|
+
tokenId,
|
|
452
|
+
grantId: input.grant.id,
|
|
453
|
+
subject: input.grant.subject,
|
|
454
|
+
clientId: input.grant.clientId,
|
|
455
|
+
resource: input.grant.resource,
|
|
456
|
+
expiresAt
|
|
457
|
+
});
|
|
458
|
+
const response = {
|
|
459
|
+
access_token: accessToken,
|
|
460
|
+
token_type: "Bearer",
|
|
461
|
+
expires_in: Math.floor(accessTokenTtlMs / 1000),
|
|
462
|
+
scope: input.grant.scopes.join(" ")
|
|
463
|
+
};
|
|
464
|
+
if (input.includeRefreshToken) {
|
|
465
|
+
const refreshToken = randomToken();
|
|
466
|
+
await options.store.putRefreshToken({
|
|
467
|
+
tokenHash: hashToken(refreshToken),
|
|
468
|
+
familyId: input.familyId ?? randomToken(),
|
|
469
|
+
grantId: input.grant.id,
|
|
470
|
+
clientId: input.grant.clientId,
|
|
471
|
+
subject: input.grant.subject,
|
|
472
|
+
resource: input.grant.resource,
|
|
473
|
+
scopes: input.grant.scopes,
|
|
474
|
+
createdAt: currentTime,
|
|
475
|
+
expiresAt: currentTime + refreshTokenTtlMs,
|
|
476
|
+
status: "active"
|
|
477
|
+
});
|
|
478
|
+
response.refresh_token = refreshToken;
|
|
479
|
+
}
|
|
480
|
+
return response;
|
|
481
|
+
}
|
|
482
|
+
async function exchangeAuthorizationCode(body) {
|
|
483
|
+
const codeValue = body.get("code");
|
|
484
|
+
if (codeValue === null)
|
|
485
|
+
throw new OAuthProtocolError("invalid_request", "code is required.");
|
|
486
|
+
const code = await options.store.takeAuthorizationCode(hashToken(codeValue));
|
|
487
|
+
const currentTime = now();
|
|
488
|
+
if (code === undefined || code.expiresAt <= currentTime) {
|
|
489
|
+
throw new OAuthProtocolError("invalid_grant", "Authorization code is invalid.");
|
|
490
|
+
}
|
|
491
|
+
if (body.get("client_id") !== code.clientId ||
|
|
492
|
+
body.get("redirect_uri") !== code.redirectUri ||
|
|
493
|
+
requireResource(body.get("resource")) !== code.resource ||
|
|
494
|
+
!pkceMatches(body.get("code_verifier") ?? "", code.codeChallenge)) {
|
|
495
|
+
throw new OAuthProtocolError("invalid_grant", "Authorization code binding is invalid.");
|
|
496
|
+
}
|
|
497
|
+
const grant = await options.store.getGrant(code.grantId);
|
|
498
|
+
if (grant === undefined || grant.revokedAt !== undefined) {
|
|
499
|
+
throw new OAuthProtocolError("invalid_grant", "Authorization grant is invalid.");
|
|
500
|
+
}
|
|
501
|
+
return formResponse(await issueToken({
|
|
502
|
+
grant,
|
|
503
|
+
includeRefreshToken: grant.scopes.includes("offline_access")
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
async function rotateRefreshToken(body) {
|
|
507
|
+
const refreshToken = body.get("refresh_token");
|
|
508
|
+
if (refreshToken === null) {
|
|
509
|
+
throw new OAuthProtocolError("invalid_request", "refresh_token is required.");
|
|
510
|
+
}
|
|
511
|
+
const replacementValue = randomToken();
|
|
512
|
+
const currentTime = now();
|
|
513
|
+
const replacementTokenHash = hashToken(replacementValue);
|
|
514
|
+
const requestedResource = requireResource(body.get("resource"));
|
|
515
|
+
const tokenHash = hashToken(refreshToken);
|
|
516
|
+
const existing = await options.store.rotateRefreshToken(tokenHash, replacementTokenHash, currentTime, currentTime + refreshTokenTtlMs);
|
|
517
|
+
if (existing.status !== "rotated") {
|
|
518
|
+
throw new OAuthProtocolError("invalid_grant", "Refresh token is invalid or replayed.");
|
|
519
|
+
}
|
|
520
|
+
if (existing.previous.clientId !== body.get("client_id") ||
|
|
521
|
+
existing.previous.resource !== requestedResource) {
|
|
522
|
+
await options.store.revokeGrant(existing.previous.grantId, currentTime);
|
|
523
|
+
throw new OAuthProtocolError("invalid_grant", "Refresh token binding is invalid.");
|
|
524
|
+
}
|
|
525
|
+
const grant = await options.store.getGrant(existing.previous.grantId);
|
|
526
|
+
if (grant === undefined || grant.revokedAt !== undefined) {
|
|
527
|
+
throw new OAuthProtocolError("invalid_grant", "Authorization grant is invalid.");
|
|
528
|
+
}
|
|
529
|
+
const response = await issueToken({ grant, includeRefreshToken: false });
|
|
530
|
+
response.refresh_token = replacementValue;
|
|
531
|
+
return formResponse(response);
|
|
532
|
+
}
|
|
533
|
+
async function handleToken(request) {
|
|
534
|
+
requireFormContentType(request);
|
|
535
|
+
const body = new URLSearchParams(await readRequestBody(request));
|
|
536
|
+
const grantType = body.get("grant_type");
|
|
537
|
+
if (grantType === "authorization_code")
|
|
538
|
+
return exchangeAuthorizationCode(body);
|
|
539
|
+
if (grantType === "refresh_token")
|
|
540
|
+
return rotateRefreshToken(body);
|
|
541
|
+
throw new OAuthProtocolError("unsupported_grant_type", "Unsupported grant_type.");
|
|
542
|
+
}
|
|
543
|
+
async function handleRevoke(request) {
|
|
544
|
+
requireFormContentType(request);
|
|
545
|
+
const body = new URLSearchParams(await readRequestBody(request));
|
|
546
|
+
const token = body.get("token");
|
|
547
|
+
if (token !== null)
|
|
548
|
+
await options.store.revokeToken(hashToken(token), now());
|
|
549
|
+
return new Response(null, { status: 200, headers: { "cache-control": "no-store" } });
|
|
550
|
+
}
|
|
551
|
+
async function handle(request) {
|
|
552
|
+
try {
|
|
553
|
+
const url = new URL(request.url);
|
|
554
|
+
if (request.method === "GET" && url.pathname === "/.well-known/oauth-authorization-server") {
|
|
555
|
+
return formResponse({
|
|
556
|
+
issuer,
|
|
557
|
+
authorization_endpoint: endpoint("/authorize"),
|
|
558
|
+
token_endpoint: endpoint("/token"),
|
|
559
|
+
registration_endpoint: endpoint("/register"),
|
|
560
|
+
revocation_endpoint: endpoint("/revoke"),
|
|
561
|
+
jwks_uri: endpoint("/.well-known/jwks.json"),
|
|
562
|
+
response_types_supported: ["code"],
|
|
563
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
564
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
565
|
+
code_challenge_methods_supported: ["S256"],
|
|
566
|
+
protected_resources: [...resources]
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (request.method === "GET" && url.pathname === "/.well-known/jwks.json") {
|
|
570
|
+
return formResponse({ keys: publishedJwks });
|
|
571
|
+
}
|
|
572
|
+
if (request.method === "POST" && url.pathname === "/register") {
|
|
573
|
+
return await handleRegister(request);
|
|
574
|
+
}
|
|
575
|
+
if (request.method === "GET" && url.pathname === "/authorize") {
|
|
576
|
+
return await handleAuthorize(request);
|
|
577
|
+
}
|
|
578
|
+
if (request.method === "POST" && url.pathname === "/token") {
|
|
579
|
+
return await handleToken(request);
|
|
580
|
+
}
|
|
581
|
+
if (request.method === "POST" && url.pathname === "/revoke") {
|
|
582
|
+
return await handleRevoke(request);
|
|
583
|
+
}
|
|
584
|
+
return formResponse({ error: "not_found" }, 404);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
return protocolErrorResponse(error);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function verifyAccessToken(token, resource) {
|
|
591
|
+
const normalizedResource = parseAbsoluteUrl(resource, "resource");
|
|
592
|
+
if (!resources.has(normalizedResource)) {
|
|
593
|
+
throw new Error("Access token resource is not supported.");
|
|
594
|
+
}
|
|
595
|
+
const storedToken = await options.store.getAccessToken(hashToken(token));
|
|
596
|
+
const currentTime = now();
|
|
597
|
+
if (storedToken === undefined ||
|
|
598
|
+
storedToken.revokedAt !== undefined ||
|
|
599
|
+
storedToken.expiresAt <= currentTime) {
|
|
600
|
+
throw new Error("Access token is revoked, expired, or unknown.");
|
|
601
|
+
}
|
|
602
|
+
const verified = await jwtVerify(token, await verificationKey, {
|
|
603
|
+
issuer,
|
|
604
|
+
audience: normalizedResource,
|
|
605
|
+
algorithms: [options.signingKey.algorithm],
|
|
606
|
+
typ: "at+jwt"
|
|
607
|
+
});
|
|
608
|
+
const subject = verified.payload.sub;
|
|
609
|
+
const clientId = verified.payload.client_id;
|
|
610
|
+
const tokenId = verified.payload.jti;
|
|
611
|
+
const scope = verified.payload.scope;
|
|
612
|
+
if (typeof subject !== "string" ||
|
|
613
|
+
typeof clientId !== "string" ||
|
|
614
|
+
typeof tokenId !== "string" ||
|
|
615
|
+
typeof scope !== "string" ||
|
|
616
|
+
subject !== storedToken.subject ||
|
|
617
|
+
clientId !== storedToken.clientId ||
|
|
618
|
+
tokenId !== storedToken.tokenId ||
|
|
619
|
+
normalizedResource !== storedToken.resource) {
|
|
620
|
+
throw new Error("Access token claims do not match the authorization record.");
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
subject,
|
|
624
|
+
clientId,
|
|
625
|
+
resource: normalizedResource,
|
|
626
|
+
scopes: parseScopes(scope),
|
|
627
|
+
tokenId,
|
|
628
|
+
expiresAt: Math.floor(storedToken.expiresAt / 1000)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
issuer,
|
|
633
|
+
handle,
|
|
634
|
+
completeAuthorization,
|
|
635
|
+
denyAuthorization,
|
|
636
|
+
verifyAccessToken,
|
|
637
|
+
async revokeGrant(grantId) {
|
|
638
|
+
await options.store.revokeGrant(grantId, now());
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"description": "Production OAuth authorization server primitives for MCP applications",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node ../../scripts/guard-package-dist.mjs && tsc",
|
|
15
|
+
"test": "cd ../.. && vitest run packages/mcp-oauth-server/src",
|
|
16
|
+
"test:unit": "cd ../.. && vitest run packages/mcp-oauth-server/src"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.18"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/poe-platform/poe-code.git",
|
|
28
|
+
"directory": "packages/mcp-oauth-server"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"mcp-oauth": "*"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.126",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"test": "cd ../.. && vitest run packages/toolcraft/src",
|
|
101
101
|
"test:unit": "cd ../.. && vitest run packages/toolcraft/src",
|
|
102
102
|
"lint": "cd ../.. && eslint packages/toolcraft/src --ext ts && tsc -p packages/toolcraft/tsconfig.json --noEmit",
|
|
103
|
-
"prepack": "node ../../scripts/manage-bundled-workspace-deps.mjs prepare . toolcraft-schema toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner tiny-http-mcp-server tiny-mcp-client tiny-stdio-mcp-server auth-store jose fast-string-width fast-wrap-ansi ignore jsonc-parser sisteransi smol-toml yaml",
|
|
104
|
-
"postpack": "node ../../scripts/manage-bundled-workspace-deps.mjs cleanup . toolcraft-schema toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner tiny-http-mcp-server tiny-mcp-client tiny-stdio-mcp-server auth-store jose fast-string-width fast-wrap-ansi ignore jsonc-parser sisteransi smol-toml yaml"
|
|
103
|
+
"prepack": "node ../../scripts/manage-bundled-workspace-deps.mjs prepare . toolcraft-schema toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner mcp-oauth-server tiny-http-mcp-server tiny-mcp-client tiny-stdio-mcp-server auth-store jose fast-string-width fast-wrap-ansi ignore jsonc-parser sisteransi smol-toml yaml",
|
|
104
|
+
"postpack": "node ../../scripts/manage-bundled-workspace-deps.mjs cleanup . toolcraft-schema toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner mcp-oauth-server tiny-http-mcp-server tiny-mcp-client tiny-stdio-mcp-server auth-store jose fast-string-width fast-wrap-ansi ignore jsonc-parser sisteransi smol-toml yaml"
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
107
|
"commander": "^13.1.0",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"@poe-code/agent-defs",
|
|
139
139
|
"@poe-code/config-mutations",
|
|
140
140
|
"@poe-code/process-runner",
|
|
141
|
+
"mcp-oauth-server",
|
|
141
142
|
"tiny-http-mcp-server",
|
|
142
143
|
"tiny-mcp-client",
|
|
143
144
|
"tiny-stdio-mcp-server",
|
|
@@ -152,7 +153,7 @@
|
|
|
152
153
|
"yaml"
|
|
153
154
|
],
|
|
154
155
|
"optionalDependencies": {
|
|
155
|
-
"toolcraft-schema": "0.0.
|
|
156
|
+
"toolcraft-schema": "0.0.126",
|
|
156
157
|
"toolcraft-design": "*",
|
|
157
158
|
"@poe-code/frontmatter": "*",
|
|
158
159
|
"@poe-code/agent-mcp-config": "*",
|
|
@@ -161,6 +162,7 @@
|
|
|
161
162
|
"@poe-code/agent-defs": "*",
|
|
162
163
|
"@poe-code/config-mutations": "*",
|
|
163
164
|
"@poe-code/process-runner": "*",
|
|
165
|
+
"mcp-oauth-server": "*",
|
|
164
166
|
"tiny-http-mcp-server": "*",
|
|
165
167
|
"tiny-mcp-client": "*",
|
|
166
168
|
"tiny-stdio-mcp-server": "*",
|