@webpieces/core-util 0.4.880 → 0.4.882
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 +1 -1
- package/src/errors/ApiCallSite.d.ts +58 -0
- package/src/errors/ApiCallSite.js +96 -0
- package/src/errors/ApiCallSite.js.map +1 -0
- package/src/errors/ApiError.d.ts +9 -0
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ReportableApiFailure.d.ts +43 -0
- package/src/errors/ReportableApiFailure.js +75 -0
- package/src/errors/ReportableApiFailure.js.map +1 -0
- package/src/errors/index.d.ts +3 -0
- package/src/errors/index.js +6 -1
- package/src/errors/index.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +3 -0
- package/src/http/WebpiecesDefaultErrorTranslator.js +10 -0
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/http/WebpiecesDefaultFailureClassifier.d.ts +3 -2
- package/src/http/WebpiecesDefaultFailureClassifier.js +6 -3
- package/src/http/WebpiecesDefaultFailureClassifier.js.map +1 -1
- package/src/http/api-type.js +2 -2
- package/src/http/api-type.js.map +1 -1
- package/src/index.d.ts +8 -2
- package/src/index.js +20 -1
- package/src/index.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +14 -4
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +25 -13
- package/src/mcp/McpMetadata.js +31 -14
- package/src/mcp/McpMetadata.js.map +1 -1
- package/src/mcp/McpToolCatalogFile.d.ts +14 -2
- package/src/mcp/McpToolCatalogFile.js +24 -3
- package/src/mcp/McpToolCatalogFile.js.map +1 -1
- package/src/oauth/OAuthAuthorizationErrorRedirect.d.ts +37 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js +61 -0
- package/src/oauth/OAuthAuthorizationErrorRedirect.js.map +1 -0
- package/src/oauth/OAuthBearerChallenge.d.ts +31 -0
- package/src/oauth/OAuthBearerChallenge.js +52 -0
- package/src/oauth/OAuthBearerChallenge.js.map +1 -0
- package/src/oauth/OAuthErrorCode.d.ts +69 -0
- package/src/oauth/OAuthErrorCode.js +89 -0
- package/src/oauth/OAuthErrorCode.js.map +1 -0
- package/src/oauth/OAuthErrorResponse.d.ts +34 -0
- package/src/oauth/OAuthErrorResponse.js +74 -0
- package/src/oauth/OAuthErrorResponse.js.map +1 -0
- package/src/oauth/OAuthProtocolError.d.ts +61 -0
- package/src/oauth/OAuthProtocolError.js +83 -0
- package/src/oauth/OAuthProtocolError.js.map +1 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.d.ts +10 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js +39 -0
- package/src/oauth/OAuthProtocolErrorCompileAssertions.js.map +1 -0
|
@@ -12,8 +12,14 @@ import { WpMcpToolHints } from './McpMetadata';
|
|
|
12
12
|
* which it is made of, are already here for the same reason.
|
|
13
13
|
*/
|
|
14
14
|
export declare class McpToolDefinition {
|
|
15
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
15
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
16
16
|
readonly name: string;
|
|
17
|
+
/**
|
|
18
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
19
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
20
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
21
|
+
*/
|
|
22
|
+
readonly title: string;
|
|
17
23
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
18
24
|
readonly methodName: string;
|
|
19
25
|
/**
|
|
@@ -31,8 +37,14 @@ export declare class McpToolDefinition {
|
|
|
31
37
|
readonly inputSchema: ApiJsonSchema;
|
|
32
38
|
readonly outputSchema: ApiJsonSchema;
|
|
33
39
|
constructor(
|
|
34
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
40
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
35
41
|
name: string,
|
|
42
|
+
/**
|
|
43
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
44
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
45
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
46
|
+
*/
|
|
47
|
+
title: string,
|
|
36
48
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
37
49
|
methodName: string,
|
|
38
50
|
/**
|
|
@@ -16,14 +16,21 @@ const McpMetadata_1 = require("./McpMetadata");
|
|
|
16
16
|
*/
|
|
17
17
|
class McpToolDefinition {
|
|
18
18
|
name;
|
|
19
|
+
title;
|
|
19
20
|
methodName;
|
|
20
21
|
description;
|
|
21
22
|
hints;
|
|
22
23
|
inputSchema;
|
|
23
24
|
outputSchema;
|
|
24
25
|
constructor(
|
|
25
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
26
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
26
27
|
name,
|
|
28
|
+
/**
|
|
29
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
30
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
31
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
32
|
+
*/
|
|
33
|
+
title,
|
|
27
34
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
28
35
|
methodName,
|
|
29
36
|
/**
|
|
@@ -39,6 +46,7 @@ class McpToolDefinition {
|
|
|
39
46
|
*/
|
|
40
47
|
hints, inputSchema, outputSchema) {
|
|
41
48
|
this.name = name;
|
|
49
|
+
this.title = title;
|
|
42
50
|
this.methodName = methodName;
|
|
43
51
|
this.description = description;
|
|
44
52
|
this.hints = hints;
|
|
@@ -100,7 +108,7 @@ class McpToolCatalogFile {
|
|
|
100
108
|
for (const tool of tools) {
|
|
101
109
|
if (byName.has(tool.name)) {
|
|
102
110
|
throw new McpToolCatalogError(`Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`, 'Tool names are the protocol identity and must be globally unique — rename one ' +
|
|
103
|
-
"of the two @WpMcpTool('
|
|
111
|
+
"of the two @WpMcpTool('<name>', '<title>') declarations.");
|
|
104
112
|
}
|
|
105
113
|
byName.set(tool.name, tool);
|
|
106
114
|
}
|
|
@@ -175,7 +183,7 @@ class McpCatalogJson {
|
|
|
175
183
|
toolFrom(entry) {
|
|
176
184
|
const record = this.record(entry, 'a tool definition');
|
|
177
185
|
const hints = this.record(record['hints'], 'a tool definition hints block');
|
|
178
|
-
return new McpToolDefinition(this.text(record['name'], 'name'), this.text(record['methodName'], 'methodName'), this.text(record['description'], 'description'), new McpMetadata_1.WpMcpToolHints(hints['readOnlyHint'] === true, hints['destructiveHint'] === true, hints['idempotentHint'] === true, hints['openWorldHint'] === true), this.schemaFrom(record['inputSchema']), this.schemaFrom(record['outputSchema']));
|
|
186
|
+
return new McpToolDefinition(this.text(record['name'], 'name'), this.title(record['title'], record['name']), this.text(record['methodName'], 'methodName'), this.text(record['description'], 'description'), new McpMetadata_1.WpMcpToolHints(hints['readOnlyHint'] === true, hints['destructiveHint'] === true, hints['idempotentHint'] === true, hints['openWorldHint'] === true), this.schemaFrom(record['inputSchema']), this.schemaFrom(record['outputSchema']));
|
|
179
187
|
}
|
|
180
188
|
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
181
189
|
schemaFrom(value) {
|
|
@@ -240,6 +248,19 @@ class McpCatalogJson {
|
|
|
240
248
|
// webpieces-disable no-any-unknown -- the narrowed result of the check above
|
|
241
249
|
return value;
|
|
242
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The REQUIRED, non-blank `title` (#1180). A catalog generated before titles existed has none, and
|
|
253
|
+
* is refused here so a stale artifact fails the boot loudly instead of publishing untitled tools.
|
|
254
|
+
*/
|
|
255
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
256
|
+
title(value, name) {
|
|
257
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
258
|
+
throw new McpToolCatalogError(`The MCP tool catalog has no 'title' on tool '${String(name)}'; every tool needs a ` +
|
|
259
|
+
"human-readable title from @WpMcpTool('<name>', '<title>'). The catalog predates " +
|
|
260
|
+
'required titles (#1180).', REGENERATE);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
243
264
|
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
244
265
|
text(value, field) {
|
|
245
266
|
if (typeof value !== 'string') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpToolCatalogFile.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpToolCatalogFile.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiJsonSchema, ApiJsonSchemaDiscriminator, ApiJsonSchemaType } from './DtoSchema';\nimport { WpMcpToolHints } from './McpMetadata';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it.\n *\n * It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and\n * READ here at boot — so the tool an agent is shown and the tool the server accepts are the same\n * bytes by construction, not by two declarations agreeing.\n *\n * It lives in `core-util` rather than in the extractor package because both ends need it and only\n * one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},\n * which it is made of, are already here for the same reason.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool('...')`. */\n readonly name: string,\n /** The contract method it was rendered from, so a mismatch report can name the source. */\n readonly methodName: string,\n /**\n * The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.\n * Documentation has exactly ONE source — the doc comment above the declaration — for the\n * partner reading OpenAPI and the agent reading `tools/list` alike.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by\n * `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes\n * from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n\n/** Thrown when a catalog is not one this release can read or write. */\nexport class McpToolCatalogError extends Error {\n constructor(\n message: string,\n /** What to do instead, in one sentence. */\n readonly cure: string,\n ) {\n super(`${message} ${cure}`);\n this.name = 'McpToolCatalogError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */\nconst REGENERATE =\n 'Rebuild the api library that declares the contract (its `openapi-generate` target runs ' +\n '`wp-openapi`, which writes one `mcp-<ContractClass>-tools.json` per MCP contract).';\n\nconst FILE_PREFIX = 'mcp-';\nconst FILE_SUFFIX = '-tools.json';\n\n/** A class name, which is what the file name carries. Anything else is not one of these files. */\nconst CONTRACT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * ONE contract's MCP tools, keyed by protocol name — the artifact `wp-openapi` writes as\n * `mcp-<ContractClass>-tools.json`, one file per contract that declares `MCP` and has at least one\n * `@WpMcpTool`, beside the library's OpenAPI documents.\n *\n * ## Why one file per CONTRACT and not one per library\n *\n * One api library holds many contracts (it is how a public partner API is grouped), and a server binds\n * contracts, not libraries — `McpApiBinding.local(LangCourseAuthorApi, ...)`. With one file per\n * contract the server can check every binding against exactly the file generated from THAT contract,\n * and refuse a file it was handed for a contract it does not bind. A single per-library file could only\n * say \"some tool is missing\" (#1021).\n *\n * This is the browser-safe CODEC — the bytes and the file name. Reading the files from an installed\n * package is `McpToolCatalog.fromPackages` in `@webpieces/mcp-server`, because only a node process\n * has a filesystem to read them from.\n */\nexport class McpToolCatalogFile {\n private readonly byName: ReadonlyMap<string, McpToolDefinition>;\n\n constructor(\n /** The contract CLASS name — `binding.api.name` at runtime, the class declaration at build time. */\n readonly contractName: string,\n readonly tools: readonly McpToolDefinition[],\n ) {\n if (!CONTRACT_NAME.test(contractName)) {\n throw new McpToolCatalogError(\n `'${contractName}' is not a contract class name, so it cannot name an MCP tool catalog.`,\n REGENERATE,\n );\n }\n const byName = new Map<string, McpToolDefinition>();\n for (const tool of tools) {\n if (byName.has(tool.name)) {\n throw new McpToolCatalogError(\n `Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`,\n 'Tool names are the protocol identity and must be globally unique — rename one ' +\n \"of the two @WpMcpTool('...') declarations.\",\n );\n }\n byName.set(tool.name, tool);\n }\n this.byName = byName;\n }\n\n /** `mcp-<ContractClass>-tools.json`. */\n get fileName(): string {\n return McpToolCatalogFile.fileNameFor(this.contractName);\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.byName.get(name);\n }\n\n /** The names this catalog carries, sorted, so a failure can print what WAS published. */\n names(): readonly string[] {\n return [...this.byName.keys()].sort();\n }\n\n /** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */\n toJsonText(): string {\n const tools = [...this.tools].sort((left: McpToolDefinition, right: McpToolDefinition) =>\n left.name.localeCompare(right.name),\n );\n return `${JSON.stringify(tools, undefined, 2)}\\n`;\n }\n\n /** The file name the catalog of `contractName` is written to and read from. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static fileNameFor(contractName: string): string {\n return `${FILE_PREFIX}${contractName}${FILE_SUFFIX}`;\n }\n\n /** The contract a file name is the catalog of, or `undefined` when it is not one of these files. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static contractNameOf(fileName: string): string | undefined {\n if (!fileName.startsWith(FILE_PREFIX) || !fileName.endsWith(FILE_SUFFIX)) return undefined;\n const name = fileName.slice(FILE_PREFIX.length, fileName.length - FILE_SUFFIX.length);\n return CONTRACT_NAME.test(name) ? name : undefined;\n }\n\n /**\n * Reads one `mcp-<ContractClass>-tools.json`. The contract is the one the FILE NAME names. Every\n * nested value is rebuilt as a real class instance, because this is the one place untyped bytes\n * become the schemas a live protocol surface publishes.\n *\n * Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level\n * handler, which is where every other read-the-world failure in webpieces is reported.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromJsonText(fileName: string, text: string): McpToolCatalogFile {\n const contractName = McpToolCatalogFile.contractNameOf(fileName);\n if (contractName === undefined) {\n throw new McpToolCatalogError(\n `'${fileName}' is not an MCP tool catalog file name; they are named ` +\n `${FILE_PREFIX}<ContractClass>${FILE_SUFFIX}.`,\n REGENERATE,\n );\n }\n return new McpToolCatalogFile(contractName, new McpCatalogJson().parse(text));\n }\n}\n\n/**\n * The PARSE BOUNDARY: untyped bytes in, real class instances out.\n *\n * A class of its own rather than statics on {@link McpToolCatalogFile} so every step is an ordinary\n * instance method — and because it is the one place in this file where `unknown` is the correct type\n * rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.\n */\nclass McpCatalogJson {\n parse(text: string): readonly McpToolDefinition[] {\n // webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new McpToolCatalogError(\n 'The MCP tool catalog must be a JSON array of tool definitions.',\n REGENERATE,\n );\n }\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n return parsed.map((entry: unknown) => this.toolFrom(entry));\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private toolFrom(entry: unknown): McpToolDefinition {\n const record = this.record(entry, 'a tool definition');\n const hints = this.record(record['hints'], 'a tool definition hints block');\n return new McpToolDefinition(\n this.text(record['name'], 'name'),\n this.text(record['methodName'], 'methodName'),\n this.text(record['description'], 'description'),\n new WpMcpToolHints(\n hints['readOnlyHint'] === true,\n hints['destructiveHint'] === true,\n hints['idempotentHint'] === true,\n hints['openWorldHint'] === true,\n ),\n this.schemaFrom(record['inputSchema']),\n this.schemaFrom(record['outputSchema']),\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private schemaFrom(value: unknown): ApiJsonSchema {\n const record = this.record(value, 'a JSON schema');\n const schema = new ApiJsonSchema(\n record['type'] as ApiJsonSchemaType | readonly ApiJsonSchemaType[],\n );\n if (typeof record['description'] === 'string') schema.description = record['description'];\n if (typeof record['minimum'] === 'number') schema.minimum = record['minimum'];\n if (typeof record['maximum'] === 'number') schema.maximum = record['maximum'];\n if (Array.isArray(record['required'])) schema.required = record['required'] as string[];\n if (Array.isArray(record['enum'])) schema.enum = record['enum'] as string[];\n if (typeof record['x-mcp-header'] === 'string') {\n schema['x-mcp-header'] = record['x-mcp-header'];\n }\n if (record['items'] !== undefined) schema.items = this.schemaFrom(record['items']);\n const extra = record['additionalProperties'];\n if (typeof extra === 'boolean') schema.additionalProperties = extra;\n else if (extra !== undefined) schema.additionalProperties = this.schemaFrom(extra);\n const properties = record['properties'];\n if (properties !== undefined) {\n const built: Record<string, ApiJsonSchema> = {};\n for (const [key, child] of Object.entries(\n this.record(properties, 'a schema properties block'),\n )) {\n built[key] = this.schemaFrom(child);\n }\n schema.properties = built;\n }\n const oneOf = record['oneOf'];\n if (Array.isArray(oneOf)) {\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n schema.oneOf = oneOf.map((branch: unknown) => this.schemaFrom(branch));\n schema.discriminator = this.discriminatorFrom(record['discriminator']);\n }\n return schema;\n }\n\n /**\n * The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT\n * one, so an absent block is a legal document and not a missing field.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private discriminatorFrom(value: unknown): ApiJsonSchemaDiscriminator | undefined {\n if (value === undefined) return undefined;\n const record = this.record(value, 'a schema discriminator block');\n const mapping: Record<string, string> = {};\n for (const [key, branch] of Object.entries(\n this.record(record['mapping'], 'a discriminator mapping block'),\n )) {\n mapping[key] = this.text(branch, `discriminator mapping '${key}'`);\n }\n return new ApiJsonSchemaDiscriminator(\n this.text(record['propertyName'], 'discriminator propertyName'),\n mapping,\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private record(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);\n }\n // webpieces-disable no-any-unknown -- the narrowed result of the check above\n return value as Record<string, unknown>;\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private text(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no '${field}' on a tool definition.`,\n REGENERATE,\n );\n }\n return value;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"McpToolCatalogFile.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpToolCatalogFile.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiJsonSchema, ApiJsonSchemaDiscriminator, ApiJsonSchemaType } from './DtoSchema';\nimport { WpMcpToolHints } from './McpMetadata';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it.\n *\n * It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and\n * READ here at boot — so the tool an agent is shown and the tool the server accepts are the same\n * bytes by construction, not by two declarations agreeing.\n *\n * It lives in `core-util` rather than in the extractor package because both ends need it and only\n * one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},\n * which it is made of, are already here for the same reason.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */\n readonly name: string,\n /**\n * The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):\n * `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry\n * without one is refused on load rather than silently publishing an untitled tool.\n */\n readonly title: string,\n /** The contract method it was rendered from, so a mismatch report can name the source. */\n readonly methodName: string,\n /**\n * The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.\n * Documentation has exactly ONE source — the doc comment above the declaration — for the\n * partner reading OpenAPI and the agent reading `tools/list` alike.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by\n * `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes\n * from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n\n/** Thrown when a catalog is not one this release can read or write. */\nexport class McpToolCatalogError extends Error {\n constructor(\n message: string,\n /** What to do instead, in one sentence. */\n readonly cure: string,\n ) {\n super(`${message} ${cure}`);\n this.name = 'McpToolCatalogError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */\nconst REGENERATE =\n 'Rebuild the api library that declares the contract (its `openapi-generate` target runs ' +\n '`wp-openapi`, which writes one `mcp-<ContractClass>-tools.json` per MCP contract).';\n\nconst FILE_PREFIX = 'mcp-';\nconst FILE_SUFFIX = '-tools.json';\n\n/** A class name, which is what the file name carries. Anything else is not one of these files. */\nconst CONTRACT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * ONE contract's MCP tools, keyed by protocol name — the artifact `wp-openapi` writes as\n * `mcp-<ContractClass>-tools.json`, one file per contract that declares `MCP` and has at least one\n * `@WpMcpTool`, beside the library's OpenAPI documents.\n *\n * ## Why one file per CONTRACT and not one per library\n *\n * One api library holds many contracts (it is how a public partner API is grouped), and a server binds\n * contracts, not libraries — `McpApiBinding.local(LangCourseAuthorApi, ...)`. With one file per\n * contract the server can check every binding against exactly the file generated from THAT contract,\n * and refuse a file it was handed for a contract it does not bind. A single per-library file could only\n * say \"some tool is missing\" (#1021).\n *\n * This is the browser-safe CODEC — the bytes and the file name. Reading the files from an installed\n * package is `McpToolCatalog.fromPackages` in `@webpieces/mcp-server`, because only a node process\n * has a filesystem to read them from.\n */\nexport class McpToolCatalogFile {\n private readonly byName: ReadonlyMap<string, McpToolDefinition>;\n\n constructor(\n /** The contract CLASS name — `binding.api.name` at runtime, the class declaration at build time. */\n readonly contractName: string,\n readonly tools: readonly McpToolDefinition[],\n ) {\n if (!CONTRACT_NAME.test(contractName)) {\n throw new McpToolCatalogError(\n `'${contractName}' is not a contract class name, so it cannot name an MCP tool catalog.`,\n REGENERATE,\n );\n }\n const byName = new Map<string, McpToolDefinition>();\n for (const tool of tools) {\n if (byName.has(tool.name)) {\n throw new McpToolCatalogError(\n `Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`,\n 'Tool names are the protocol identity and must be globally unique — rename one ' +\n \"of the two @WpMcpTool('<name>', '<title>') declarations.\",\n );\n }\n byName.set(tool.name, tool);\n }\n this.byName = byName;\n }\n\n /** `mcp-<ContractClass>-tools.json`. */\n get fileName(): string {\n return McpToolCatalogFile.fileNameFor(this.contractName);\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.byName.get(name);\n }\n\n /** The names this catalog carries, sorted, so a failure can print what WAS published. */\n names(): readonly string[] {\n return [...this.byName.keys()].sort();\n }\n\n /** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */\n toJsonText(): string {\n const tools = [...this.tools].sort((left: McpToolDefinition, right: McpToolDefinition) =>\n left.name.localeCompare(right.name),\n );\n return `${JSON.stringify(tools, undefined, 2)}\\n`;\n }\n\n /** The file name the catalog of `contractName` is written to and read from. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static fileNameFor(contractName: string): string {\n return `${FILE_PREFIX}${contractName}${FILE_SUFFIX}`;\n }\n\n /** The contract a file name is the catalog of, or `undefined` when it is not one of these files. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static contractNameOf(fileName: string): string | undefined {\n if (!fileName.startsWith(FILE_PREFIX) || !fileName.endsWith(FILE_SUFFIX)) return undefined;\n const name = fileName.slice(FILE_PREFIX.length, fileName.length - FILE_SUFFIX.length);\n return CONTRACT_NAME.test(name) ? name : undefined;\n }\n\n /**\n * Reads one `mcp-<ContractClass>-tools.json`. The contract is the one the FILE NAME names. Every\n * nested value is rebuilt as a real class instance, because this is the one place untyped bytes\n * become the schemas a live protocol surface publishes.\n *\n * Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level\n * handler, which is where every other read-the-world failure in webpieces is reported.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromJsonText(fileName: string, text: string): McpToolCatalogFile {\n const contractName = McpToolCatalogFile.contractNameOf(fileName);\n if (contractName === undefined) {\n throw new McpToolCatalogError(\n `'${fileName}' is not an MCP tool catalog file name; they are named ` +\n `${FILE_PREFIX}<ContractClass>${FILE_SUFFIX}.`,\n REGENERATE,\n );\n }\n return new McpToolCatalogFile(contractName, new McpCatalogJson().parse(text));\n }\n}\n\n/**\n * The PARSE BOUNDARY: untyped bytes in, real class instances out.\n *\n * A class of its own rather than statics on {@link McpToolCatalogFile} so every step is an ordinary\n * instance method — and because it is the one place in this file where `unknown` is the correct type\n * rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.\n */\nclass McpCatalogJson {\n parse(text: string): readonly McpToolDefinition[] {\n // webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new McpToolCatalogError(\n 'The MCP tool catalog must be a JSON array of tool definitions.',\n REGENERATE,\n );\n }\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n return parsed.map((entry: unknown) => this.toolFrom(entry));\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private toolFrom(entry: unknown): McpToolDefinition {\n const record = this.record(entry, 'a tool definition');\n const hints = this.record(record['hints'], 'a tool definition hints block');\n return new McpToolDefinition(\n this.text(record['name'], 'name'),\n this.title(record['title'], record['name']),\n this.text(record['methodName'], 'methodName'),\n this.text(record['description'], 'description'),\n new WpMcpToolHints(\n hints['readOnlyHint'] === true,\n hints['destructiveHint'] === true,\n hints['idempotentHint'] === true,\n hints['openWorldHint'] === true,\n ),\n this.schemaFrom(record['inputSchema']),\n this.schemaFrom(record['outputSchema']),\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private schemaFrom(value: unknown): ApiJsonSchema {\n const record = this.record(value, 'a JSON schema');\n const schema = new ApiJsonSchema(\n record['type'] as ApiJsonSchemaType | readonly ApiJsonSchemaType[],\n );\n if (typeof record['description'] === 'string') schema.description = record['description'];\n if (typeof record['minimum'] === 'number') schema.minimum = record['minimum'];\n if (typeof record['maximum'] === 'number') schema.maximum = record['maximum'];\n if (Array.isArray(record['required'])) schema.required = record['required'] as string[];\n if (Array.isArray(record['enum'])) schema.enum = record['enum'] as string[];\n if (typeof record['x-mcp-header'] === 'string') {\n schema['x-mcp-header'] = record['x-mcp-header'];\n }\n if (record['items'] !== undefined) schema.items = this.schemaFrom(record['items']);\n const extra = record['additionalProperties'];\n if (typeof extra === 'boolean') schema.additionalProperties = extra;\n else if (extra !== undefined) schema.additionalProperties = this.schemaFrom(extra);\n const properties = record['properties'];\n if (properties !== undefined) {\n const built: Record<string, ApiJsonSchema> = {};\n for (const [key, child] of Object.entries(\n this.record(properties, 'a schema properties block'),\n )) {\n built[key] = this.schemaFrom(child);\n }\n schema.properties = built;\n }\n const oneOf = record['oneOf'];\n if (Array.isArray(oneOf)) {\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n schema.oneOf = oneOf.map((branch: unknown) => this.schemaFrom(branch));\n schema.discriminator = this.discriminatorFrom(record['discriminator']);\n }\n return schema;\n }\n\n /**\n * The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT\n * one, so an absent block is a legal document and not a missing field.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private discriminatorFrom(value: unknown): ApiJsonSchemaDiscriminator | undefined {\n if (value === undefined) return undefined;\n const record = this.record(value, 'a schema discriminator block');\n const mapping: Record<string, string> = {};\n for (const [key, branch] of Object.entries(\n this.record(record['mapping'], 'a discriminator mapping block'),\n )) {\n mapping[key] = this.text(branch, `discriminator mapping '${key}'`);\n }\n return new ApiJsonSchemaDiscriminator(\n this.text(record['propertyName'], 'discriminator propertyName'),\n mapping,\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private record(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);\n }\n // webpieces-disable no-any-unknown -- the narrowed result of the check above\n return value as Record<string, unknown>;\n }\n\n /**\n * The REQUIRED, non-blank `title` (#1180). A catalog generated before titles existed has none, and\n * is refused here so a stale artifact fails the boot loudly instead of publishing untitled tools.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private title(value: unknown, name: unknown): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no 'title' on tool '${String(name)}'; every tool needs a ` +\n \"human-readable title from @WpMcpTool('<name>', '<title>'). The catalog predates \" +\n 'required titles (#1180).',\n REGENERATE,\n );\n }\n return value;\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private text(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no '${field}' on a tool definition.`,\n REGENERATE,\n );\n }\n return value;\n }\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AuthorizationEndpointErrorCode } from './OAuthErrorCode';
|
|
2
|
+
/**
|
|
3
|
+
* The AUTHORIZATION endpoint's error answer, RFC 6749 §4.1.2.1: not a status code at all, but a
|
|
4
|
+
* redirect of the user agent back to the client's `redirect_uri` carrying
|
|
5
|
+
* `error`, `error_description?`, `error_uri?`, `state` and (RFC 9207) `iss?` as query parameters.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const url = new OAuthAuthorizationErrorRedirect(
|
|
9
|
+
* client.redirectUri, OAuthErrorCode.ACCESS_DENIED, requestState, 'The user declined',
|
|
10
|
+
* ).toUrl();
|
|
11
|
+
* res.redirect(302, url);
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `requestState` above is the request's `state` narrowed to `string | undefined` (express types a
|
|
15
|
+
* query value more widely). `state` is a REQUIRED argument that may be `undefined`: the RFC requires it back exactly when the
|
|
16
|
+
* client sent it, so every caller must hand over what the request carried rather than forget it.
|
|
17
|
+
* An existing query on `redirectUri` is kept, as the RFC requires.
|
|
18
|
+
*
|
|
19
|
+
* Only redirect to a `redirect_uri` you have VALIDATED against the client's registration. When the
|
|
20
|
+
* `redirect_uri` or `client_id` is missing, invalid or mismatched, RFC 6749 §4.1.2.1 forbids the
|
|
21
|
+
* redirect (it would be an open redirector) — answer the resource owner directly instead, for
|
|
22
|
+
* example with an `OAuthProtocolError` rendered as JSON.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OAuthAuthorizationErrorRedirect {
|
|
25
|
+
readonly redirectUri: string;
|
|
26
|
+
readonly error: AuthorizationEndpointErrorCode;
|
|
27
|
+
readonly state: string | undefined;
|
|
28
|
+
readonly description?: string | undefined;
|
|
29
|
+
readonly errorUri?: string | undefined;
|
|
30
|
+
/** RFC 9207 `iss`: the authorization server's issuer identifier, for mix-up defence. */
|
|
31
|
+
readonly issuer?: string | undefined;
|
|
32
|
+
constructor(redirectUri: string, error: AuthorizationEndpointErrorCode, state: string | undefined, description?: string | undefined, errorUri?: string | undefined,
|
|
33
|
+
/** RFC 9207 `iss`: the authorization server's issuer identifier, for mix-up defence. */
|
|
34
|
+
issuer?: string | undefined);
|
|
35
|
+
/** The absolute redirect URL. Throws `TypeError` (from `URL`) if `redirectUri` is not absolute. */
|
|
36
|
+
toUrl(): string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuthAuthorizationErrorRedirect = void 0;
|
|
4
|
+
const OAuthErrorCode_1 = require("./OAuthErrorCode");
|
|
5
|
+
/**
|
|
6
|
+
* The AUTHORIZATION endpoint's error answer, RFC 6749 §4.1.2.1: not a status code at all, but a
|
|
7
|
+
* redirect of the user agent back to the client's `redirect_uri` carrying
|
|
8
|
+
* `error`, `error_description?`, `error_uri?`, `state` and (RFC 9207) `iss?` as query parameters.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const url = new OAuthAuthorizationErrorRedirect(
|
|
12
|
+
* client.redirectUri, OAuthErrorCode.ACCESS_DENIED, requestState, 'The user declined',
|
|
13
|
+
* ).toUrl();
|
|
14
|
+
* res.redirect(302, url);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `requestState` above is the request's `state` narrowed to `string | undefined` (express types a
|
|
18
|
+
* query value more widely). `state` is a REQUIRED argument that may be `undefined`: the RFC requires it back exactly when the
|
|
19
|
+
* client sent it, so every caller must hand over what the request carried rather than forget it.
|
|
20
|
+
* An existing query on `redirectUri` is kept, as the RFC requires.
|
|
21
|
+
*
|
|
22
|
+
* Only redirect to a `redirect_uri` you have VALIDATED against the client's registration. When the
|
|
23
|
+
* `redirect_uri` or `client_id` is missing, invalid or mismatched, RFC 6749 §4.1.2.1 forbids the
|
|
24
|
+
* redirect (it would be an open redirector) — answer the resource owner directly instead, for
|
|
25
|
+
* example with an `OAuthProtocolError` rendered as JSON.
|
|
26
|
+
*/
|
|
27
|
+
class OAuthAuthorizationErrorRedirect {
|
|
28
|
+
redirectUri;
|
|
29
|
+
error;
|
|
30
|
+
state;
|
|
31
|
+
description;
|
|
32
|
+
errorUri;
|
|
33
|
+
issuer;
|
|
34
|
+
constructor(redirectUri, error, state, description, errorUri,
|
|
35
|
+
/** RFC 9207 `iss`: the authorization server's issuer identifier, for mix-up defence. */
|
|
36
|
+
issuer) {
|
|
37
|
+
this.redirectUri = redirectUri;
|
|
38
|
+
this.error = error;
|
|
39
|
+
this.state = state;
|
|
40
|
+
this.description = description;
|
|
41
|
+
this.errorUri = errorUri;
|
|
42
|
+
this.issuer = issuer;
|
|
43
|
+
}
|
|
44
|
+
/** The absolute redirect URL. Throws `TypeError` (from `URL`) if `redirectUri` is not absolute. */
|
|
45
|
+
toUrl() {
|
|
46
|
+
const url = new URL(this.redirectUri);
|
|
47
|
+
url.searchParams.set('error', this.error);
|
|
48
|
+
if (this.description !== undefined) {
|
|
49
|
+
url.searchParams.set('error_description', OAuthErrorCode_1.OAuthText.sanitize(this.description));
|
|
50
|
+
}
|
|
51
|
+
if (this.errorUri !== undefined)
|
|
52
|
+
url.searchParams.set('error_uri', this.errorUri);
|
|
53
|
+
if (this.state !== undefined)
|
|
54
|
+
url.searchParams.set('state', this.state);
|
|
55
|
+
if (this.issuer !== undefined)
|
|
56
|
+
url.searchParams.set('iss', this.issuer);
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.OAuthAuthorizationErrorRedirect = OAuthAuthorizationErrorRedirect;
|
|
61
|
+
//# sourceMappingURL=OAuthAuthorizationErrorRedirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthAuthorizationErrorRedirect.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/oauth/OAuthAuthorizationErrorRedirect.ts"],"names":[],"mappings":";;;AAAA,qDAA6E;AAE7E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,+BAA+B;IAEpB;IACA;IACA;IACA;IACA;IAEA;IAPpB,YACoB,WAAmB,EACnB,KAAqC,EACrC,KAAyB,EACzB,WAAoB,EACpB,QAAiB;IACjC,wFAAwF;IACxE,MAAe;QANf,gBAAW,GAAX,WAAW,CAAQ;QACnB,UAAK,GAAL,KAAK,CAAgC;QACrC,UAAK,GAAL,KAAK,CAAoB;QACzB,gBAAW,GAAX,WAAW,CAAS;QACpB,aAAQ,GAAR,QAAQ,CAAS;QAEjB,WAAM,GAAN,MAAM,CAAS;IAChC,CAAC;IAEJ,mGAAmG;IACnG,KAAK;QACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACjC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,EAAE,0BAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClF,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACxE,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACxE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;CACJ;AAvBD,0EAuBC","sourcesContent":["import { AuthorizationEndpointErrorCode, OAuthText } from './OAuthErrorCode';\n\n/**\n * The AUTHORIZATION endpoint's error answer, RFC 6749 §4.1.2.1: not a status code at all, but a\n * redirect of the user agent back to the client's `redirect_uri` carrying\n * `error`, `error_description?`, `error_uri?`, `state` and (RFC 9207) `iss?` as query parameters.\n *\n * ```ts\n * const url = new OAuthAuthorizationErrorRedirect(\n * client.redirectUri, OAuthErrorCode.ACCESS_DENIED, requestState, 'The user declined',\n * ).toUrl();\n * res.redirect(302, url);\n * ```\n *\n * `requestState` above is the request's `state` narrowed to `string | undefined` (express types a\n * query value more widely). `state` is a REQUIRED argument that may be `undefined`: the RFC requires it back exactly when the\n * client sent it, so every caller must hand over what the request carried rather than forget it.\n * An existing query on `redirectUri` is kept, as the RFC requires.\n *\n * Only redirect to a `redirect_uri` you have VALIDATED against the client's registration. When the\n * `redirect_uri` or `client_id` is missing, invalid or mismatched, RFC 6749 §4.1.2.1 forbids the\n * redirect (it would be an open redirector) — answer the resource owner directly instead, for\n * example with an `OAuthProtocolError` rendered as JSON.\n */\nexport class OAuthAuthorizationErrorRedirect {\n constructor(\n public readonly redirectUri: string,\n public readonly error: AuthorizationEndpointErrorCode,\n public readonly state: string | undefined,\n public readonly description?: string,\n public readonly errorUri?: string,\n /** RFC 9207 `iss`: the authorization server's issuer identifier, for mix-up defence. */\n public readonly issuer?: string,\n ) {}\n\n /** The absolute redirect URL. Throws `TypeError` (from `URL`) if `redirectUri` is not absolute. */\n toUrl(): string {\n const url = new URL(this.redirectUri);\n url.searchParams.set('error', this.error);\n if (this.description !== undefined) {\n url.searchParams.set('error_description', OAuthText.sanitize(this.description));\n }\n if (this.errorUri !== undefined) url.searchParams.set('error_uri', this.errorUri);\n if (this.state !== undefined) url.searchParams.set('state', this.state);\n if (this.issuer !== undefined) url.searchParams.set('iss', this.issuer);\n return url.toString();\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BearerErrorCode } from './OAuthErrorCode';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the RFC 6750 §3 `WWW-Authenticate: Bearer ...` value a protected resource answers a refused
|
|
4
|
+
* bearer token with:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* new OAuthBearerChallenge(OAuthErrorCode.INVALID_TOKEN, 'The access token expired', undefined, 'mcp').toHeaderValue();
|
|
8
|
+
* // Bearer realm="mcp", error="invalid_token", error_description="The access token expired"
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* `error` is `undefined` for a request that carried NO credential at all: RFC 6750 §3.1 says such a
|
|
12
|
+
* challenge SHOULD NOT include an error code. `resourceMetadata` is the RFC 9728 attribute MCP
|
|
13
|
+
* clients follow to discover the authorization server. Every attribute value is sanitized to the
|
|
14
|
+
* RFC's quoted-string character set, so app text can never break the header.
|
|
15
|
+
*/
|
|
16
|
+
export declare class OAuthBearerChallenge {
|
|
17
|
+
readonly error: BearerErrorCode | undefined;
|
|
18
|
+
readonly description?: string | undefined;
|
|
19
|
+
/** Space-delimited scopes the resource requires (RFC 6750 `scope`), for `insufficient_scope`. */
|
|
20
|
+
readonly scope?: string | undefined;
|
|
21
|
+
readonly realm?: string | undefined;
|
|
22
|
+
/** RFC 9728 `resource_metadata`: the protected-resource metadata URL. */
|
|
23
|
+
readonly resourceMetadata?: string | undefined;
|
|
24
|
+
constructor(error: BearerErrorCode | undefined, description?: string | undefined,
|
|
25
|
+
/** Space-delimited scopes the resource requires (RFC 6750 `scope`), for `insufficient_scope`. */
|
|
26
|
+
scope?: string | undefined, realm?: string | undefined,
|
|
27
|
+
/** RFC 9728 `resource_metadata`: the protected-resource metadata URL. */
|
|
28
|
+
resourceMetadata?: string | undefined);
|
|
29
|
+
toHeaderValue(): string;
|
|
30
|
+
private add;
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuthBearerChallenge = void 0;
|
|
4
|
+
const OAuthErrorCode_1 = require("./OAuthErrorCode");
|
|
5
|
+
/**
|
|
6
|
+
* Builds the RFC 6750 §3 `WWW-Authenticate: Bearer ...` value a protected resource answers a refused
|
|
7
|
+
* bearer token with:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* new OAuthBearerChallenge(OAuthErrorCode.INVALID_TOKEN, 'The access token expired', undefined, 'mcp').toHeaderValue();
|
|
11
|
+
* // Bearer realm="mcp", error="invalid_token", error_description="The access token expired"
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `error` is `undefined` for a request that carried NO credential at all: RFC 6750 §3.1 says such a
|
|
15
|
+
* challenge SHOULD NOT include an error code. `resourceMetadata` is the RFC 9728 attribute MCP
|
|
16
|
+
* clients follow to discover the authorization server. Every attribute value is sanitized to the
|
|
17
|
+
* RFC's quoted-string character set, so app text can never break the header.
|
|
18
|
+
*/
|
|
19
|
+
class OAuthBearerChallenge {
|
|
20
|
+
error;
|
|
21
|
+
description;
|
|
22
|
+
scope;
|
|
23
|
+
realm;
|
|
24
|
+
resourceMetadata;
|
|
25
|
+
constructor(error, description,
|
|
26
|
+
/** Space-delimited scopes the resource requires (RFC 6750 `scope`), for `insufficient_scope`. */
|
|
27
|
+
scope, realm,
|
|
28
|
+
/** RFC 9728 `resource_metadata`: the protected-resource metadata URL. */
|
|
29
|
+
resourceMetadata) {
|
|
30
|
+
this.error = error;
|
|
31
|
+
this.description = description;
|
|
32
|
+
this.scope = scope;
|
|
33
|
+
this.realm = realm;
|
|
34
|
+
this.resourceMetadata = resourceMetadata;
|
|
35
|
+
}
|
|
36
|
+
toHeaderValue() {
|
|
37
|
+
const attributes = [];
|
|
38
|
+
this.add(attributes, 'realm', this.realm);
|
|
39
|
+
this.add(attributes, 'error', this.error);
|
|
40
|
+
this.add(attributes, 'error_description', this.description);
|
|
41
|
+
this.add(attributes, 'scope', this.scope);
|
|
42
|
+
this.add(attributes, 'resource_metadata', this.resourceMetadata);
|
|
43
|
+
return attributes.length === 0 ? 'Bearer' : `Bearer ${attributes.join(', ')}`;
|
|
44
|
+
}
|
|
45
|
+
add(attributes, name, value) {
|
|
46
|
+
if (value !== undefined) {
|
|
47
|
+
attributes.push(`${name}="${OAuthErrorCode_1.OAuthText.sanitize(value)}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.OAuthBearerChallenge = OAuthBearerChallenge;
|
|
52
|
+
//# sourceMappingURL=OAuthBearerChallenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthBearerChallenge.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/oauth/OAuthBearerChallenge.ts"],"names":[],"mappings":";;;AAAA,qDAA8D;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAET;IACA;IAEA;IACA;IAEA;IAPpB,YACoB,KAAkC,EAClC,WAAoB;IACpC,iGAAiG;IACjF,KAAc,EACd,KAAc;IAC9B,yEAAyE;IACzD,gBAAyB;QANzB,UAAK,GAAL,KAAK,CAA6B;QAClC,gBAAW,GAAX,WAAW,CAAS;QAEpB,UAAK,GAAL,KAAK,CAAS;QACd,UAAK,GAAL,KAAK,CAAS;QAEd,qBAAgB,GAAhB,gBAAgB,CAAS;IAC1C,CAAC;IAEJ,aAAa;QACT,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,mBAAmB,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5D,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,mBAAmB,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACjE,OAAO,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAClF,CAAC;IAEO,GAAG,CAAC,UAAoB,EAAE,IAAY,EAAE,KAAyB;QACrE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,0BAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9D,CAAC;IACL,CAAC;CACJ;AA1BD,oDA0BC","sourcesContent":["import { BearerErrorCode, OAuthText } from './OAuthErrorCode';\n\n/**\n * Builds the RFC 6750 §3 `WWW-Authenticate: Bearer ...` value a protected resource answers a refused\n * bearer token with:\n *\n * ```ts\n * new OAuthBearerChallenge(OAuthErrorCode.INVALID_TOKEN, 'The access token expired', undefined, 'mcp').toHeaderValue();\n * // Bearer realm=\"mcp\", error=\"invalid_token\", error_description=\"The access token expired\"\n * ```\n *\n * `error` is `undefined` for a request that carried NO credential at all: RFC 6750 §3.1 says such a\n * challenge SHOULD NOT include an error code. `resourceMetadata` is the RFC 9728 attribute MCP\n * clients follow to discover the authorization server. Every attribute value is sanitized to the\n * RFC's quoted-string character set, so app text can never break the header.\n */\nexport class OAuthBearerChallenge {\n constructor(\n public readonly error: BearerErrorCode | undefined,\n public readonly description?: string,\n /** Space-delimited scopes the resource requires (RFC 6750 `scope`), for `insufficient_scope`. */\n public readonly scope?: string,\n public readonly realm?: string,\n /** RFC 9728 `resource_metadata`: the protected-resource metadata URL. */\n public readonly resourceMetadata?: string,\n ) {}\n\n toHeaderValue(): string {\n const attributes: string[] = [];\n this.add(attributes, 'realm', this.realm);\n this.add(attributes, 'error', this.error);\n this.add(attributes, 'error_description', this.description);\n this.add(attributes, 'scope', this.scope);\n this.add(attributes, 'resource_metadata', this.resourceMetadata);\n return attributes.length === 0 ? 'Bearer' : `Bearer ${attributes.join(', ')}`;\n }\n\n private add(attributes: string[], name: string, value: string | undefined): void {\n if (value !== undefined) {\n attributes.push(`${name}=\"${OAuthText.sanitize(value)}\"`);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed OAuth 2.x error vocabulary. A throw site picks a member; it cannot invent a code (an
|
|
3
|
+
* `invalid_credential` that no OAuth client understands) because the type has no such member.
|
|
4
|
+
*
|
|
5
|
+
* Sources, by the endpoint that answers with them:
|
|
6
|
+
* - RFC 6749 §5.2, the TOKEN endpoint: `invalid_request`, `invalid_client`, `invalid_grant`,
|
|
7
|
+
* `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`.
|
|
8
|
+
* - RFC 6750 §3.1, a PROTECTED RESOURCE (bearer token): `invalid_request`, `invalid_token`,
|
|
9
|
+
* `insufficient_scope`.
|
|
10
|
+
* - RFC 6749 §4.1.2.1, the AUTHORIZATION endpoint (answered as a redirect, see
|
|
11
|
+
* `OAuthAuthorizationErrorRedirect`): `invalid_request`, `unauthorized_client`, `access_denied`,
|
|
12
|
+
* `unsupported_response_type`, `invalid_scope`, `server_error`, `temporarily_unavailable`.
|
|
13
|
+
* - RFC 7591 §3.2.2, DYNAMIC CLIENT REGISTRATION (which MCP clients use to register themselves):
|
|
14
|
+
* `invalid_redirect_uri`, `invalid_client_metadata`, `invalid_software_statement`,
|
|
15
|
+
* `unapproved_software_statement`.
|
|
16
|
+
*
|
|
17
|
+
* A string enum, every member initialised with its wire value, per the one-spelling rule for API
|
|
18
|
+
* vocabularies in this repo.
|
|
19
|
+
*/
|
|
20
|
+
export declare enum OAuthErrorCode {
|
|
21
|
+
INVALID_REQUEST = "invalid_request",
|
|
22
|
+
INVALID_CLIENT = "invalid_client",
|
|
23
|
+
INVALID_GRANT = "invalid_grant",
|
|
24
|
+
UNAUTHORIZED_CLIENT = "unauthorized_client",
|
|
25
|
+
UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type",
|
|
26
|
+
INVALID_SCOPE = "invalid_scope",
|
|
27
|
+
INVALID_TOKEN = "invalid_token",
|
|
28
|
+
INSUFFICIENT_SCOPE = "insufficient_scope",
|
|
29
|
+
ACCESS_DENIED = "access_denied",
|
|
30
|
+
UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type",
|
|
31
|
+
SERVER_ERROR = "server_error",
|
|
32
|
+
TEMPORARILY_UNAVAILABLE = "temporarily_unavailable",
|
|
33
|
+
INVALID_REDIRECT_URI = "invalid_redirect_uri",
|
|
34
|
+
INVALID_CLIENT_METADATA = "invalid_client_metadata",
|
|
35
|
+
INVALID_SOFTWARE_STATEMENT = "invalid_software_statement",
|
|
36
|
+
UNAPPROVED_SOFTWARE_STATEMENT = "unapproved_software_statement"
|
|
37
|
+
}
|
|
38
|
+
/** Every status an OAuth error is answered with when it is rendered as JSON. */
|
|
39
|
+
export type OAuthHttpStatus = 400 | 401 | 403 | 500 | 503;
|
|
40
|
+
/**
|
|
41
|
+
* The two statuses RFC 6749 §5.2 permits for `invalid_client`: 401 by default, and REQUIRED (with a
|
|
42
|
+
* `WWW-Authenticate` header) when the client authenticated through the `Authorization` header; 400
|
|
43
|
+
* is allowed otherwise. It is the ONLY code whose status the RFCs leave to the server.
|
|
44
|
+
*/
|
|
45
|
+
export type InvalidClientStatus = 400 | 401;
|
|
46
|
+
/** Every code whose status is fixed by the RFCs — all of them except `invalid_client`. */
|
|
47
|
+
export type OAuthFixedStatusErrorCode = Exclude<OAuthErrorCode, OAuthErrorCode.INVALID_CLIENT>;
|
|
48
|
+
/** The codes RFC 6750 §3.1 defines for a `WWW-Authenticate: Bearer` challenge. */
|
|
49
|
+
export type BearerErrorCode = OAuthErrorCode.INVALID_REQUEST | OAuthErrorCode.INVALID_TOKEN | OAuthErrorCode.INSUFFICIENT_SCOPE;
|
|
50
|
+
/** The codes RFC 6749 §4.1.2.1 defines for the authorization endpoint's error redirect. */
|
|
51
|
+
export type AuthorizationEndpointErrorCode = OAuthErrorCode.INVALID_REQUEST | OAuthErrorCode.UNAUTHORIZED_CLIENT | OAuthErrorCode.ACCESS_DENIED | OAuthErrorCode.UNSUPPORTED_RESPONSE_TYPE | OAuthErrorCode.INVALID_SCOPE | OAuthErrorCode.SERVER_ERROR | OAuthErrorCode.TEMPORARILY_UNAVAILABLE;
|
|
52
|
+
/**
|
|
53
|
+
* The status each fixed code is answered with. The RFCs fix the token-endpoint and bearer codes;
|
|
54
|
+
* the authorization-endpoint codes are normally a REDIRECT, not a status, so their status here is
|
|
55
|
+
* only used when one is rendered as JSON (an authorization request whose `redirect_uri` could not be
|
|
56
|
+
* trusted, where RFC 6749 §4.1.2.1 forbids redirecting): the closest HTTP meaning of each.
|
|
57
|
+
*/
|
|
58
|
+
export declare class OAuthErrorStatus {
|
|
59
|
+
static of(code: OAuthFixedStatusErrorCode): OAuthHttpStatus;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* RFC 6749 restricts `error_description` (and RFC 6750 every quoted challenge attribute) to
|
|
63
|
+
* printable ASCII without `"` and `\` (%x20-21 / %x23-5B / %x5D-7E). Anything else is replaced with
|
|
64
|
+
* `?` at RENDER time — the error itself keeps the original text for the operator log — so an app's
|
|
65
|
+
* description can never break a header or the RFC's character set.
|
|
66
|
+
*/
|
|
67
|
+
export declare class OAuthText {
|
|
68
|
+
static sanitize(value: string): string;
|
|
69
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuthText = exports.OAuthErrorStatus = exports.OAuthErrorCode = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The fixed OAuth 2.x error vocabulary. A throw site picks a member; it cannot invent a code (an
|
|
6
|
+
* `invalid_credential` that no OAuth client understands) because the type has no such member.
|
|
7
|
+
*
|
|
8
|
+
* Sources, by the endpoint that answers with them:
|
|
9
|
+
* - RFC 6749 §5.2, the TOKEN endpoint: `invalid_request`, `invalid_client`, `invalid_grant`,
|
|
10
|
+
* `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`.
|
|
11
|
+
* - RFC 6750 §3.1, a PROTECTED RESOURCE (bearer token): `invalid_request`, `invalid_token`,
|
|
12
|
+
* `insufficient_scope`.
|
|
13
|
+
* - RFC 6749 §4.1.2.1, the AUTHORIZATION endpoint (answered as a redirect, see
|
|
14
|
+
* `OAuthAuthorizationErrorRedirect`): `invalid_request`, `unauthorized_client`, `access_denied`,
|
|
15
|
+
* `unsupported_response_type`, `invalid_scope`, `server_error`, `temporarily_unavailable`.
|
|
16
|
+
* - RFC 7591 §3.2.2, DYNAMIC CLIENT REGISTRATION (which MCP clients use to register themselves):
|
|
17
|
+
* `invalid_redirect_uri`, `invalid_client_metadata`, `invalid_software_statement`,
|
|
18
|
+
* `unapproved_software_statement`.
|
|
19
|
+
*
|
|
20
|
+
* A string enum, every member initialised with its wire value, per the one-spelling rule for API
|
|
21
|
+
* vocabularies in this repo.
|
|
22
|
+
*/
|
|
23
|
+
var OAuthErrorCode;
|
|
24
|
+
(function (OAuthErrorCode) {
|
|
25
|
+
OAuthErrorCode["INVALID_REQUEST"] = "invalid_request";
|
|
26
|
+
OAuthErrorCode["INVALID_CLIENT"] = "invalid_client";
|
|
27
|
+
OAuthErrorCode["INVALID_GRANT"] = "invalid_grant";
|
|
28
|
+
OAuthErrorCode["UNAUTHORIZED_CLIENT"] = "unauthorized_client";
|
|
29
|
+
OAuthErrorCode["UNSUPPORTED_GRANT_TYPE"] = "unsupported_grant_type";
|
|
30
|
+
OAuthErrorCode["INVALID_SCOPE"] = "invalid_scope";
|
|
31
|
+
OAuthErrorCode["INVALID_TOKEN"] = "invalid_token";
|
|
32
|
+
OAuthErrorCode["INSUFFICIENT_SCOPE"] = "insufficient_scope";
|
|
33
|
+
OAuthErrorCode["ACCESS_DENIED"] = "access_denied";
|
|
34
|
+
OAuthErrorCode["UNSUPPORTED_RESPONSE_TYPE"] = "unsupported_response_type";
|
|
35
|
+
OAuthErrorCode["SERVER_ERROR"] = "server_error";
|
|
36
|
+
OAuthErrorCode["TEMPORARILY_UNAVAILABLE"] = "temporarily_unavailable";
|
|
37
|
+
OAuthErrorCode["INVALID_REDIRECT_URI"] = "invalid_redirect_uri";
|
|
38
|
+
OAuthErrorCode["INVALID_CLIENT_METADATA"] = "invalid_client_metadata";
|
|
39
|
+
OAuthErrorCode["INVALID_SOFTWARE_STATEMENT"] = "invalid_software_statement";
|
|
40
|
+
OAuthErrorCode["UNAPPROVED_SOFTWARE_STATEMENT"] = "unapproved_software_statement";
|
|
41
|
+
})(OAuthErrorCode || (exports.OAuthErrorCode = OAuthErrorCode = {}));
|
|
42
|
+
/**
|
|
43
|
+
* The status each fixed code is answered with. The RFCs fix the token-endpoint and bearer codes;
|
|
44
|
+
* the authorization-endpoint codes are normally a REDIRECT, not a status, so their status here is
|
|
45
|
+
* only used when one is rendered as JSON (an authorization request whose `redirect_uri` could not be
|
|
46
|
+
* trusted, where RFC 6749 §4.1.2.1 forbids redirecting): the closest HTTP meaning of each.
|
|
47
|
+
*/
|
|
48
|
+
class OAuthErrorStatus {
|
|
49
|
+
// webpieces-disable no-function-outside-class -- pure, exhaustive code -> status table
|
|
50
|
+
static of(code) {
|
|
51
|
+
switch (code) {
|
|
52
|
+
case OAuthErrorCode.INVALID_TOKEN:
|
|
53
|
+
return 401;
|
|
54
|
+
case OAuthErrorCode.INSUFFICIENT_SCOPE:
|
|
55
|
+
case OAuthErrorCode.ACCESS_DENIED:
|
|
56
|
+
return 403;
|
|
57
|
+
case OAuthErrorCode.SERVER_ERROR:
|
|
58
|
+
return 500;
|
|
59
|
+
case OAuthErrorCode.TEMPORARILY_UNAVAILABLE:
|
|
60
|
+
return 503;
|
|
61
|
+
case OAuthErrorCode.INVALID_REQUEST:
|
|
62
|
+
case OAuthErrorCode.INVALID_GRANT:
|
|
63
|
+
case OAuthErrorCode.UNAUTHORIZED_CLIENT:
|
|
64
|
+
case OAuthErrorCode.UNSUPPORTED_GRANT_TYPE:
|
|
65
|
+
case OAuthErrorCode.INVALID_SCOPE:
|
|
66
|
+
case OAuthErrorCode.UNSUPPORTED_RESPONSE_TYPE:
|
|
67
|
+
case OAuthErrorCode.INVALID_REDIRECT_URI:
|
|
68
|
+
case OAuthErrorCode.INVALID_CLIENT_METADATA:
|
|
69
|
+
case OAuthErrorCode.INVALID_SOFTWARE_STATEMENT:
|
|
70
|
+
case OAuthErrorCode.UNAPPROVED_SOFTWARE_STATEMENT:
|
|
71
|
+
return 400;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.OAuthErrorStatus = OAuthErrorStatus;
|
|
76
|
+
/**
|
|
77
|
+
* RFC 6749 restricts `error_description` (and RFC 6750 every quoted challenge attribute) to
|
|
78
|
+
* printable ASCII without `"` and `\` (%x20-21 / %x23-5B / %x5D-7E). Anything else is replaced with
|
|
79
|
+
* `?` at RENDER time — the error itself keeps the original text for the operator log — so an app's
|
|
80
|
+
* description can never break a header or the RFC's character set.
|
|
81
|
+
*/
|
|
82
|
+
class OAuthText {
|
|
83
|
+
// webpieces-disable no-function-outside-class -- pure character-set filter shared by every OAuth renderer
|
|
84
|
+
static sanitize(value) {
|
|
85
|
+
return value.replace(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/g, '?');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.OAuthText = OAuthText;
|
|
89
|
+
//# sourceMappingURL=OAuthErrorCode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthErrorCode.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/oauth/OAuthErrorCode.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,IAAY,cAiBX;AAjBD,WAAY,cAAc;IACtB,qDAAmC,CAAA;IACnC,mDAAiC,CAAA;IACjC,iDAA+B,CAAA;IAC/B,6DAA2C,CAAA;IAC3C,mEAAiD,CAAA;IACjD,iDAA+B,CAAA;IAC/B,iDAA+B,CAAA;IAC/B,2DAAyC,CAAA;IACzC,iDAA+B,CAAA;IAC/B,yEAAuD,CAAA;IACvD,+CAA6B,CAAA;IAC7B,qEAAmD,CAAA;IACnD,+DAA6C,CAAA;IAC7C,qEAAmD,CAAA;IACnD,2EAAyD,CAAA;IACzD,iFAA+D,CAAA;AACnE,CAAC,EAjBW,cAAc,8BAAd,cAAc,QAiBzB;AA+BD;;;;;GAKG;AACH,MAAa,gBAAgB;IACzB,uFAAuF;IACvF,MAAM,CAAC,EAAE,CAAC,IAA+B;QACrC,QAAQ,IAAI,EAAE,CAAC;YACX,KAAK,cAAc,CAAC,aAAa;gBAC7B,OAAO,GAAG,CAAC;YACf,KAAK,cAAc,CAAC,kBAAkB,CAAC;YACvC,KAAK,cAAc,CAAC,aAAa;gBAC7B,OAAO,GAAG,CAAC;YACf,KAAK,cAAc,CAAC,YAAY;gBAC5B,OAAO,GAAG,CAAC;YACf,KAAK,cAAc,CAAC,uBAAuB;gBACvC,OAAO,GAAG,CAAC;YACf,KAAK,cAAc,CAAC,eAAe,CAAC;YACpC,KAAK,cAAc,CAAC,aAAa,CAAC;YAClC,KAAK,cAAc,CAAC,mBAAmB,CAAC;YACxC,KAAK,cAAc,CAAC,sBAAsB,CAAC;YAC3C,KAAK,cAAc,CAAC,aAAa,CAAC;YAClC,KAAK,cAAc,CAAC,yBAAyB,CAAC;YAC9C,KAAK,cAAc,CAAC,oBAAoB,CAAC;YACzC,KAAK,cAAc,CAAC,uBAAuB,CAAC;YAC5C,KAAK,cAAc,CAAC,0BAA0B,CAAC;YAC/C,KAAK,cAAc,CAAC,6BAA6B;gBAC7C,OAAO,GAAG,CAAC;QACnB,CAAC;IACL,CAAC;CACJ;AA1BD,4CA0BC;AAED;;;;;GAKG;AACH,MAAa,SAAS;IAClB,0GAA0G;IAC1G,MAAM,CAAC,QAAQ,CAAC,KAAa;QACzB,OAAO,KAAK,CAAC,OAAO,CAAC,iCAAiC,EAAE,GAAG,CAAC,CAAC;IACjE,CAAC;CACJ;AALD,8BAKC","sourcesContent":["/**\n * The fixed OAuth 2.x error vocabulary. A throw site picks a member; it cannot invent a code (an\n * `invalid_credential` that no OAuth client understands) because the type has no such member.\n *\n * Sources, by the endpoint that answers with them:\n * - RFC 6749 §5.2, the TOKEN endpoint: `invalid_request`, `invalid_client`, `invalid_grant`,\n * `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`.\n * - RFC 6750 §3.1, a PROTECTED RESOURCE (bearer token): `invalid_request`, `invalid_token`,\n * `insufficient_scope`.\n * - RFC 6749 §4.1.2.1, the AUTHORIZATION endpoint (answered as a redirect, see\n * `OAuthAuthorizationErrorRedirect`): `invalid_request`, `unauthorized_client`, `access_denied`,\n * `unsupported_response_type`, `invalid_scope`, `server_error`, `temporarily_unavailable`.\n * - RFC 7591 §3.2.2, DYNAMIC CLIENT REGISTRATION (which MCP clients use to register themselves):\n * `invalid_redirect_uri`, `invalid_client_metadata`, `invalid_software_statement`,\n * `unapproved_software_statement`.\n *\n * A string enum, every member initialised with its wire value, per the one-spelling rule for API\n * vocabularies in this repo.\n */\nexport enum OAuthErrorCode {\n INVALID_REQUEST = 'invalid_request',\n INVALID_CLIENT = 'invalid_client',\n INVALID_GRANT = 'invalid_grant',\n UNAUTHORIZED_CLIENT = 'unauthorized_client',\n UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type',\n INVALID_SCOPE = 'invalid_scope',\n INVALID_TOKEN = 'invalid_token',\n INSUFFICIENT_SCOPE = 'insufficient_scope',\n ACCESS_DENIED = 'access_denied',\n UNSUPPORTED_RESPONSE_TYPE = 'unsupported_response_type',\n SERVER_ERROR = 'server_error',\n TEMPORARILY_UNAVAILABLE = 'temporarily_unavailable',\n INVALID_REDIRECT_URI = 'invalid_redirect_uri',\n INVALID_CLIENT_METADATA = 'invalid_client_metadata',\n INVALID_SOFTWARE_STATEMENT = 'invalid_software_statement',\n UNAPPROVED_SOFTWARE_STATEMENT = 'unapproved_software_statement',\n}\n\n/** Every status an OAuth error is answered with when it is rendered as JSON. */\nexport type OAuthHttpStatus = 400 | 401 | 403 | 500 | 503;\n\n/**\n * The two statuses RFC 6749 §5.2 permits for `invalid_client`: 401 by default, and REQUIRED (with a\n * `WWW-Authenticate` header) when the client authenticated through the `Authorization` header; 400\n * is allowed otherwise. It is the ONLY code whose status the RFCs leave to the server.\n */\nexport type InvalidClientStatus = 400 | 401;\n\n/** Every code whose status is fixed by the RFCs — all of them except `invalid_client`. */\nexport type OAuthFixedStatusErrorCode = Exclude<OAuthErrorCode, OAuthErrorCode.INVALID_CLIENT>;\n\n/** The codes RFC 6750 §3.1 defines for a `WWW-Authenticate: Bearer` challenge. */\nexport type BearerErrorCode =\n | OAuthErrorCode.INVALID_REQUEST\n | OAuthErrorCode.INVALID_TOKEN\n | OAuthErrorCode.INSUFFICIENT_SCOPE;\n\n/** The codes RFC 6749 §4.1.2.1 defines for the authorization endpoint's error redirect. */\nexport type AuthorizationEndpointErrorCode =\n | OAuthErrorCode.INVALID_REQUEST\n | OAuthErrorCode.UNAUTHORIZED_CLIENT\n | OAuthErrorCode.ACCESS_DENIED\n | OAuthErrorCode.UNSUPPORTED_RESPONSE_TYPE\n | OAuthErrorCode.INVALID_SCOPE\n | OAuthErrorCode.SERVER_ERROR\n | OAuthErrorCode.TEMPORARILY_UNAVAILABLE;\n\n/**\n * The status each fixed code is answered with. The RFCs fix the token-endpoint and bearer codes;\n * the authorization-endpoint codes are normally a REDIRECT, not a status, so their status here is\n * only used when one is rendered as JSON (an authorization request whose `redirect_uri` could not be\n * trusted, where RFC 6749 §4.1.2.1 forbids redirecting): the closest HTTP meaning of each.\n */\nexport class OAuthErrorStatus {\n // webpieces-disable no-function-outside-class -- pure, exhaustive code -> status table\n static of(code: OAuthFixedStatusErrorCode): OAuthHttpStatus {\n switch (code) {\n case OAuthErrorCode.INVALID_TOKEN:\n return 401;\n case OAuthErrorCode.INSUFFICIENT_SCOPE:\n case OAuthErrorCode.ACCESS_DENIED:\n return 403;\n case OAuthErrorCode.SERVER_ERROR:\n return 500;\n case OAuthErrorCode.TEMPORARILY_UNAVAILABLE:\n return 503;\n case OAuthErrorCode.INVALID_REQUEST:\n case OAuthErrorCode.INVALID_GRANT:\n case OAuthErrorCode.UNAUTHORIZED_CLIENT:\n case OAuthErrorCode.UNSUPPORTED_GRANT_TYPE:\n case OAuthErrorCode.INVALID_SCOPE:\n case OAuthErrorCode.UNSUPPORTED_RESPONSE_TYPE:\n case OAuthErrorCode.INVALID_REDIRECT_URI:\n case OAuthErrorCode.INVALID_CLIENT_METADATA:\n case OAuthErrorCode.INVALID_SOFTWARE_STATEMENT:\n case OAuthErrorCode.UNAPPROVED_SOFTWARE_STATEMENT:\n return 400;\n }\n }\n}\n\n/**\n * RFC 6749 restricts `error_description` (and RFC 6750 every quoted challenge attribute) to\n * printable ASCII without `\"` and `\\` (%x20-21 / %x23-5B / %x5D-7E). Anything else is replaced with\n * `?` at RENDER time — the error itself keeps the original text for the operator log — so an app's\n * description can never break a header or the RFC's character set.\n */\nexport class OAuthText {\n // webpieces-disable no-function-outside-class -- pure character-set filter shared by every OAuth renderer\n static sanitize(value: string): string {\n return value.replace(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]/g, '?');\n }\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HttpResponseDto } from '../http/HttpResponseDto';
|
|
2
|
+
import { OAuthErrorCode } from './OAuthErrorCode';
|
|
3
|
+
import { OAuthProtocolError } from './OAuthProtocolError';
|
|
4
|
+
/**
|
|
5
|
+
* The RFC 6749 §5.2 / RFC 6750 §3 error body: `{ "error": "...", "error_description"?: "...",
|
|
6
|
+
* "error_uri"?: "..." }`. The property names ARE the wire names, snake_case, because an OAuth client
|
|
7
|
+
* parses exactly these keys. An absent optional is left off the JSON entirely.
|
|
8
|
+
*/
|
|
9
|
+
export declare class OAuthErrorBody {
|
|
10
|
+
readonly error: OAuthErrorCode;
|
|
11
|
+
readonly error_description?: string;
|
|
12
|
+
readonly error_uri?: string;
|
|
13
|
+
constructor(error: OAuthErrorCode, description?: string, errorUri?: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* {@link OAuthProtocolError} -> the exact HTTP response. ONE renderer, used by every boundary that
|
|
17
|
+
* can meet one (`WebpiecesDefaultErrorTranslator`, `OAuthExpressErrorHandler`, the MCP bearer
|
|
18
|
+
* boundary), so an OAuth client gets the same bytes whichever path the refusal took.
|
|
19
|
+
*
|
|
20
|
+
* Headers: the error's `WWW-Authenticate` when it has one; otherwise `defaultChallenge` for a 401
|
|
21
|
+
* (the MCP boundary passes its own challenge, which carries `resource_metadata`); otherwise, for
|
|
22
|
+
* `invalid_token` / `insufficient_scope`, the minimal Bearer challenge RFC 6750 §3 requires. Every
|
|
23
|
+
* response also carries `Cache-Control: no-store` and `Pragma: no-cache` (RFC 6749 §5.1), so no
|
|
24
|
+
* intermediary caches an answer about a credential.
|
|
25
|
+
*/
|
|
26
|
+
export declare class OAuthErrorResponse {
|
|
27
|
+
private static readonly REASONS;
|
|
28
|
+
/**
|
|
29
|
+
* @param defaultChallenge - the boundary's own `WWW-Authenticate` for a 401 whose error carries
|
|
30
|
+
* none, or `undefined` when the boundary has no challenge of its own.
|
|
31
|
+
*/
|
|
32
|
+
static toResponse(error: OAuthProtocolError, defaultChallenge: string | undefined): HttpResponseDto<OAuthErrorBody>;
|
|
33
|
+
private static challengeFor;
|
|
34
|
+
}
|