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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.299",
3
+ "version": "3.0.301",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- .filter((scope) => scope.length > 0);
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
- const server = createMcpOAuthTestServer(serverOptions);
182
- const handle = await server.listen(listenOptions);
183
- stdout.write(`${packageInfo.name} ${packageInfo.version}\n`);
184
- stdout.write(`MCP URL: ${handle.mcpUrl}\n`);
185
- stdout.write(`PRM URL: ${handle.prmUrl}\n`);
186
- stdout.write(`AS issuer: ${handle.oauth.issuer}\n`);
187
- stdout.write(`Resource: ${handle.resource}\n`);
188
- if (parsed.printTestToken) {
189
- const token = await handle.oauth.issueTokenFor({
190
- clientId: "demo-client",
191
- resource: handle.resource,
192
- scopes: parsed.scopes ?? ["mcp.read"],
193
- });
194
- stdout.write(`Test bearer token: ${token}\n`);
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 ?? 0;
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 (scope.length === 0 || parseScope(scope).length !== 1 || parseScope(scope)[0] !== scope) {
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 (scope.length === 0 || parseScope(scope).length !== 1 || parseScope(scope)[0] !== scope) {
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");