poe-code 3.0.299 → 3.0.301
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/tiny-http-mcp-oauth-test-server/dist/cli.js +34 -16
- package/packages/tiny-http-mcp-oauth-test-server/dist/index.js +12 -2
- package/packages/tiny-oauth-test-server/dist/cli.js +41 -3
- package/packages/tiny-oauth-test-server/dist/index.js +28 -3
package/package.json
CHANGED
|
@@ -42,6 +42,9 @@ function parsePort(value) {
|
|
|
42
42
|
if (value === undefined) {
|
|
43
43
|
return 0;
|
|
44
44
|
}
|
|
45
|
+
if (!isDecimalInteger(value)) {
|
|
46
|
+
throw new Error("--port must be an integer between 0 and 65535.");
|
|
47
|
+
}
|
|
45
48
|
const port = Number(value);
|
|
46
49
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
47
50
|
throw new Error("--port must be an integer between 0 and 65535.");
|
|
@@ -52,12 +55,18 @@ function parsePositiveInteger(value, flagName) {
|
|
|
52
55
|
if (value === undefined) {
|
|
53
56
|
return 60;
|
|
54
57
|
}
|
|
58
|
+
if (!isDecimalInteger(value)) {
|
|
59
|
+
throw new Error(`${flagName} must be a positive integer.`);
|
|
60
|
+
}
|
|
55
61
|
const parsed = Number(value);
|
|
56
62
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
57
63
|
throw new Error(`${flagName} must be a positive integer.`);
|
|
58
64
|
}
|
|
59
65
|
return parsed;
|
|
60
66
|
}
|
|
67
|
+
function isDecimalInteger(value) {
|
|
68
|
+
return value.length > 0 && [...value].every((character) => character >= "0" && character <= "9");
|
|
69
|
+
}
|
|
61
70
|
function parseAbsoluteUrl(value, flagName) {
|
|
62
71
|
if (value === undefined) {
|
|
63
72
|
return undefined;
|
|
@@ -88,8 +97,10 @@ function parseScopes(value) {
|
|
|
88
97
|
}
|
|
89
98
|
const scopes = value
|
|
90
99
|
.split(",")
|
|
91
|
-
.map((scope) => scope.trim())
|
|
92
|
-
|
|
100
|
+
.map((scope) => scope.trim());
|
|
101
|
+
if (scopes.some((scope) => scope.length === 0)) {
|
|
102
|
+
throw new Error("--scopes must not contain empty entries.");
|
|
103
|
+
}
|
|
93
104
|
if (scopes.length === 0) {
|
|
94
105
|
throw new Error("--scopes must include at least one non-empty scope.");
|
|
95
106
|
}
|
|
@@ -178,20 +189,27 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
|
178
189
|
port: parsed.port,
|
|
179
190
|
hostname: parsed.hostname,
|
|
180
191
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
let handle;
|
|
193
|
+
try {
|
|
194
|
+
const server = createMcpOAuthTestServer(serverOptions);
|
|
195
|
+
handle = await server.listen(listenOptions);
|
|
196
|
+
stdout.write(`${packageInfo.name} ${packageInfo.version}\n`);
|
|
197
|
+
stdout.write(`MCP URL: ${handle.mcpUrl}\n`);
|
|
198
|
+
stdout.write(`PRM URL: ${handle.prmUrl}\n`);
|
|
199
|
+
stdout.write(`AS issuer: ${handle.oauth.issuer}\n`);
|
|
200
|
+
stdout.write(`Resource: ${handle.resource}\n`);
|
|
201
|
+
if (parsed.printTestToken) {
|
|
202
|
+
const token = await handle.oauth.issueTokenFor({
|
|
203
|
+
clientId: "demo-client",
|
|
204
|
+
resource: handle.resource,
|
|
205
|
+
scopes: parsed.scopes ?? ["mcp.read"],
|
|
206
|
+
});
|
|
207
|
+
stdout.write(`Test bearer token: ${token}\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP_TEXT}\n`);
|
|
212
|
+
return 1;
|
|
195
213
|
}
|
|
196
214
|
await (dependencies.waitForShutdown ?? waitForShutdown)(handle.close);
|
|
197
215
|
return 0;
|
|
@@ -85,6 +85,9 @@ function normalizeScopes(scopes) {
|
|
|
85
85
|
if (normalizedScopes.some((scope) => scope.trim().length === 0)) {
|
|
86
86
|
throw new Error("scopes must contain non-empty values");
|
|
87
87
|
}
|
|
88
|
+
if (normalizedScopes.some((scope) => scope.trim() !== scope || scope.includes(" "))) {
|
|
89
|
+
throw new Error("scope entries must not contain spaces");
|
|
90
|
+
}
|
|
88
91
|
return normalizedScopes;
|
|
89
92
|
}
|
|
90
93
|
function normalizeTtlSeconds(ttlSeconds) {
|
|
@@ -94,6 +97,13 @@ function normalizeTtlSeconds(ttlSeconds) {
|
|
|
94
97
|
}
|
|
95
98
|
return normalizedTtlSeconds;
|
|
96
99
|
}
|
|
100
|
+
function normalizeListenPort(port) {
|
|
101
|
+
const normalizedPort = port ?? 0;
|
|
102
|
+
if (!Number.isInteger(normalizedPort) || normalizedPort < 0 || normalizedPort > 65535) {
|
|
103
|
+
throw new Error("port must be an integer between 0 and 65535");
|
|
104
|
+
}
|
|
105
|
+
return normalizedPort;
|
|
106
|
+
}
|
|
97
107
|
function closeServer(server) {
|
|
98
108
|
return new Promise((resolve, reject) => {
|
|
99
109
|
server.close((error) => {
|
|
@@ -135,9 +145,9 @@ export function createMcpOAuthTestServer(options = {}) {
|
|
|
135
145
|
if (currentHandle !== null || listenPending) {
|
|
136
146
|
throw new Error("MCP OAuth test server is already listening");
|
|
137
147
|
}
|
|
138
|
-
listenPending = true;
|
|
139
148
|
const hostname = listenOptions.hostname ?? "127.0.0.1";
|
|
140
|
-
const requestedPort = listenOptions.port
|
|
149
|
+
const requestedPort = normalizeListenPort(listenOptions.port);
|
|
150
|
+
listenPending = true;
|
|
141
151
|
let lastError;
|
|
142
152
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
143
153
|
try {
|
|
@@ -108,16 +108,27 @@ function parseScope(value) {
|
|
|
108
108
|
function formatScope(scope) {
|
|
109
109
|
return scope.length === 0 ? void 0 : scope.join(" ");
|
|
110
110
|
}
|
|
111
|
+
function isValidScopeEntry(scope) {
|
|
112
|
+
const parsed = parseScope(scope);
|
|
113
|
+
return scope.length > 0 && parsed.length === 1 && parsed[0] === scope;
|
|
114
|
+
}
|
|
111
115
|
function validateConfiguredScopes(scopes) {
|
|
112
116
|
if (scopes === void 0) {
|
|
113
117
|
return;
|
|
114
118
|
}
|
|
115
119
|
for (const scope of scopes) {
|
|
116
|
-
if (
|
|
120
|
+
if (!isValidScopeEntry(scope)) {
|
|
117
121
|
throw new Error("scope entries must not contain spaces");
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
}
|
|
125
|
+
function validateDirectTokenScopes(scopes) {
|
|
126
|
+
for (const scope of scopes) {
|
|
127
|
+
if (!isValidScopeEntry(scope)) {
|
|
128
|
+
throw new OAuthRequestError(400, "invalid_request", "scope entries must not contain spaces");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
121
132
|
function createRandomToken() {
|
|
122
133
|
return randomBytes(24).toString("base64url");
|
|
123
134
|
}
|
|
@@ -341,6 +352,16 @@ function requireJsonContentType(headers) {
|
|
|
341
352
|
);
|
|
342
353
|
}
|
|
343
354
|
}
|
|
355
|
+
function requireFormContentType(headers) {
|
|
356
|
+
const contentType = headers["content-type"]?.split(";")[0]?.trim().toLowerCase();
|
|
357
|
+
if (contentType !== "application/x-www-form-urlencoded") {
|
|
358
|
+
throw new OAuthRequestError(
|
|
359
|
+
400,
|
|
360
|
+
"invalid_request",
|
|
361
|
+
"Content-Type must be application/x-www-form-urlencoded"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
344
365
|
function normalizeRegistrationStringArray(value, field, fallback) {
|
|
345
366
|
if (value === void 0) {
|
|
346
367
|
return [...fallback];
|
|
@@ -391,7 +412,7 @@ function assertAllowedScopes(requestedScopes, client) {
|
|
|
391
412
|
}
|
|
392
413
|
}
|
|
393
414
|
function normalizeStaticClient(input) {
|
|
394
|
-
if (input.clientId.length === 0) {
|
|
415
|
+
if (input.clientId.trim().length === 0) {
|
|
395
416
|
throw new Error("staticClients[].clientId must be non-empty");
|
|
396
417
|
}
|
|
397
418
|
if (input.redirectUris.length === 0) {
|
|
@@ -526,11 +547,15 @@ function createOAuthTestServer(options = {}) {
|
|
|
526
547
|
},
|
|
527
548
|
async issueTokenFor(input) {
|
|
528
549
|
const issuer = getIssuer();
|
|
550
|
+
if (input.clientId.trim().length === 0) {
|
|
551
|
+
throw new Error("clientId must be non-empty");
|
|
552
|
+
}
|
|
529
553
|
const resource = parseAbsoluteUrl(input.resource, "resource");
|
|
530
554
|
const ttlSeconds = input.ttlSeconds ?? defaultTokenTtlSeconds;
|
|
531
555
|
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
532
556
|
throw new Error("ttlSeconds must be a positive integer");
|
|
533
557
|
}
|
|
558
|
+
validateDirectTokenScopes(input.scopes);
|
|
534
559
|
const token = await issueAccessToken({
|
|
535
560
|
issuer,
|
|
536
561
|
clientId: input.clientId,
|
|
@@ -623,6 +648,7 @@ function createOAuthTestServer(options = {}) {
|
|
|
623
648
|
if (method === "POST" && paths.tokenPaths.includes(url.pathname)) {
|
|
624
649
|
const body = await readBody(request);
|
|
625
650
|
appendRequestLog(body);
|
|
651
|
+
requireFormContentType(requestHeaders);
|
|
626
652
|
await handleToken(new URLSearchParams(body), response);
|
|
627
653
|
return;
|
|
628
654
|
}
|
|
@@ -902,7 +928,7 @@ function createOAuthTestServer(options = {}) {
|
|
|
902
928
|
const resource = getOwnEntry(payload, "resource");
|
|
903
929
|
const ttlSeconds = getOwnEntry(payload, "ttl_seconds");
|
|
904
930
|
const scopes = getOwnEntry(payload, "scopes");
|
|
905
|
-
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
931
|
+
if (typeof clientId !== "string" || clientId.trim().length === 0) {
|
|
906
932
|
throw new OAuthRequestError(400, "invalid_request", "client_id is required");
|
|
907
933
|
}
|
|
908
934
|
if (typeof resource !== "string" || resource.length === 0) {
|
|
@@ -912,6 +938,9 @@ function createOAuthTestServer(options = {}) {
|
|
|
912
938
|
throw new OAuthRequestError(400, "invalid_request", "scopes must be a string or array");
|
|
913
939
|
}
|
|
914
940
|
const parsedScopes = typeof scopes === "string" ? parseScope(scopes) : scopes ?? [];
|
|
941
|
+
if (Array.isArray(scopes)) {
|
|
942
|
+
validateDirectTokenScopes(parsedScopes);
|
|
943
|
+
}
|
|
915
944
|
if (ttlSeconds !== void 0 && (typeof ttlSeconds !== "number" || !Number.isInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
916
945
|
throw new OAuthRequestError(
|
|
917
946
|
400,
|
|
@@ -1107,6 +1136,9 @@ function parsePort(value) {
|
|
|
1107
1136
|
if (value === void 0) {
|
|
1108
1137
|
return 0;
|
|
1109
1138
|
}
|
|
1139
|
+
if (!isDecimalInteger(value)) {
|
|
1140
|
+
throw new Error("--port must be an integer between 0 and 65535.");
|
|
1141
|
+
}
|
|
1110
1142
|
const port = Number(value);
|
|
1111
1143
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1112
1144
|
throw new Error("--port must be an integer between 0 and 65535.");
|
|
@@ -1117,12 +1149,18 @@ function parsePositiveInteger(value, flagName) {
|
|
|
1117
1149
|
if (value === void 0) {
|
|
1118
1150
|
return 60;
|
|
1119
1151
|
}
|
|
1152
|
+
if (!isDecimalInteger(value)) {
|
|
1153
|
+
throw new Error(`${flagName} must be a positive integer.`);
|
|
1154
|
+
}
|
|
1120
1155
|
const parsed = Number(value);
|
|
1121
1156
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1122
1157
|
throw new Error(`${flagName} must be a positive integer.`);
|
|
1123
1158
|
}
|
|
1124
1159
|
return parsed;
|
|
1125
1160
|
}
|
|
1161
|
+
function isDecimalInteger(value) {
|
|
1162
|
+
return value.length > 0 && [...value].every((character) => character >= "0" && character <= "9");
|
|
1163
|
+
}
|
|
1126
1164
|
function parseIssuer(value) {
|
|
1127
1165
|
if (value === void 0) {
|
|
1128
1166
|
return void 0;
|
|
@@ -106,16 +106,27 @@ function parseScope(value) {
|
|
|
106
106
|
function formatScope(scope) {
|
|
107
107
|
return scope.length === 0 ? undefined : scope.join(" ");
|
|
108
108
|
}
|
|
109
|
+
function isValidScopeEntry(scope) {
|
|
110
|
+
const parsed = parseScope(scope);
|
|
111
|
+
return scope.length > 0 && parsed.length === 1 && parsed[0] === scope;
|
|
112
|
+
}
|
|
109
113
|
function validateConfiguredScopes(scopes) {
|
|
110
114
|
if (scopes === undefined) {
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
113
117
|
for (const scope of scopes) {
|
|
114
|
-
if (
|
|
118
|
+
if (!isValidScopeEntry(scope)) {
|
|
115
119
|
throw new Error("scope entries must not contain spaces");
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
123
|
+
function validateDirectTokenScopes(scopes) {
|
|
124
|
+
for (const scope of scopes) {
|
|
125
|
+
if (!isValidScopeEntry(scope)) {
|
|
126
|
+
throw new OAuthRequestError(400, "invalid_request", "scope entries must not contain spaces");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
119
130
|
function createRandomToken() {
|
|
120
131
|
return randomBytes(24).toString("base64url");
|
|
121
132
|
}
|
|
@@ -347,6 +358,12 @@ function requireJsonContentType(headers) {
|
|
|
347
358
|
throw new OAuthRequestError(400, "invalid_request", "Content-Type must be application/json");
|
|
348
359
|
}
|
|
349
360
|
}
|
|
361
|
+
function requireFormContentType(headers) {
|
|
362
|
+
const contentType = headers["content-type"]?.split(";")[0]?.trim().toLowerCase();
|
|
363
|
+
if (contentType !== "application/x-www-form-urlencoded") {
|
|
364
|
+
throw new OAuthRequestError(400, "invalid_request", "Content-Type must be application/x-www-form-urlencoded");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
350
367
|
function normalizeRegistrationStringArray(value, field, fallback) {
|
|
351
368
|
if (value === undefined) {
|
|
352
369
|
return [...fallback];
|
|
@@ -398,7 +415,7 @@ function assertAllowedScopes(requestedScopes, client) {
|
|
|
398
415
|
}
|
|
399
416
|
}
|
|
400
417
|
function normalizeStaticClient(input) {
|
|
401
|
-
if (input.clientId.length === 0) {
|
|
418
|
+
if (input.clientId.trim().length === 0) {
|
|
402
419
|
throw new Error("staticClients[].clientId must be non-empty");
|
|
403
420
|
}
|
|
404
421
|
if (input.redirectUris.length === 0) {
|
|
@@ -532,11 +549,15 @@ export function createOAuthTestServer(options = {}) {
|
|
|
532
549
|
},
|
|
533
550
|
async issueTokenFor(input) {
|
|
534
551
|
const issuer = getIssuer();
|
|
552
|
+
if (input.clientId.trim().length === 0) {
|
|
553
|
+
throw new Error("clientId must be non-empty");
|
|
554
|
+
}
|
|
535
555
|
const resource = parseAbsoluteUrl(input.resource, "resource");
|
|
536
556
|
const ttlSeconds = input.ttlSeconds ?? defaultTokenTtlSeconds;
|
|
537
557
|
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
538
558
|
throw new Error("ttlSeconds must be a positive integer");
|
|
539
559
|
}
|
|
560
|
+
validateDirectTokenScopes(input.scopes);
|
|
540
561
|
const token = await issueAccessToken({
|
|
541
562
|
issuer,
|
|
542
563
|
clientId: input.clientId,
|
|
@@ -629,6 +650,7 @@ export function createOAuthTestServer(options = {}) {
|
|
|
629
650
|
if (method === "POST" && paths.tokenPaths.includes(url.pathname)) {
|
|
630
651
|
const body = await readBody(request);
|
|
631
652
|
appendRequestLog(body);
|
|
653
|
+
requireFormContentType(requestHeaders);
|
|
632
654
|
await handleToken(new URLSearchParams(body), response);
|
|
633
655
|
return;
|
|
634
656
|
}
|
|
@@ -847,7 +869,7 @@ export function createOAuthTestServer(options = {}) {
|
|
|
847
869
|
const resource = getOwnEntry(payload, "resource");
|
|
848
870
|
const ttlSeconds = getOwnEntry(payload, "ttl_seconds");
|
|
849
871
|
const scopes = getOwnEntry(payload, "scopes");
|
|
850
|
-
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
872
|
+
if (typeof clientId !== "string" || clientId.trim().length === 0) {
|
|
851
873
|
throw new OAuthRequestError(400, "invalid_request", "client_id is required");
|
|
852
874
|
}
|
|
853
875
|
if (typeof resource !== "string" || resource.length === 0) {
|
|
@@ -857,6 +879,9 @@ export function createOAuthTestServer(options = {}) {
|
|
|
857
879
|
throw new OAuthRequestError(400, "invalid_request", "scopes must be a string or array");
|
|
858
880
|
}
|
|
859
881
|
const parsedScopes = typeof scopes === "string" ? parseScope(scopes) : scopes ?? [];
|
|
882
|
+
if (Array.isArray(scopes)) {
|
|
883
|
+
validateDirectTokenScopes(parsedScopes);
|
|
884
|
+
}
|
|
860
885
|
if (ttlSeconds !== undefined
|
|
861
886
|
&& (typeof ttlSeconds !== "number" || !Number.isInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
862
887
|
throw new OAuthRequestError(400, "invalid_request", "ttl_seconds must be a positive integer");
|