@noodleseed/one 0.157.0 → 0.159.0
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/README.md +8 -5
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +4 -0
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/local-readiness.d.ts.map +1 -1
- package/dist/commands/local-readiness.js +8 -0
- package/dist/commands/local-readiness.js.map +1 -1
- package/dist/commands/project-setup.d.ts.map +1 -1
- package/dist/commands/project-setup.js +83 -47
- package/dist/commands/project-setup.js.map +1 -1
- package/dist/first-run.d.ts +1 -0
- package/dist/first-run.d.ts.map +1 -1
- package/dist/first-run.js +119 -46
- package/dist/first-run.js.map +1 -1
- package/dist/import-scaffold.d.ts +5 -0
- package/dist/import-scaffold.d.ts.map +1 -0
- package/dist/import-scaffold.js +41 -0
- package/dist/import-scaffold.js.map +1 -0
- package/dist/mcp-import.d.ts.map +1 -1
- package/dist/mcp-import.js +18 -15
- package/dist/mcp-import.js.map +1 -1
- package/dist/openapi-import.d.ts.map +1 -1
- package/dist/openapi-import.js +106 -53
- package/dist/openapi-import.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts +142 -3
- package/dist/plugin-mode/plugin-operation-contract.d.ts.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.js +13 -2
- package/dist/plugin-mode/plugin-operation-contract.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-tools.d.ts.map +1 -1
- package/dist/plugin-mode/plugin-operation-tools.js +27 -8
- package/dist/plugin-mode/plugin-operation-tools.js.map +1 -1
- package/dist/project-bootstrap-install.d.ts +14 -0
- package/dist/project-bootstrap-install.d.ts.map +1 -0
- package/dist/project-bootstrap-install.js +73 -0
- package/dist/project-bootstrap-install.js.map +1 -0
- package/dist/project-bootstrap-launch.d.ts +27 -0
- package/dist/project-bootstrap-launch.d.ts.map +1 -0
- package/dist/project-bootstrap-launch.js +64 -0
- package/dist/project-bootstrap-launch.js.map +1 -0
- package/dist/project-bootstrap-process.d.ts +19 -0
- package/dist/project-bootstrap-process.d.ts.map +1 -0
- package/dist/project-bootstrap-process.js +60 -0
- package/dist/project-bootstrap-process.js.map +1 -0
- package/dist/project-bootstrap-report.d.ts +55 -0
- package/dist/project-bootstrap-report.d.ts.map +1 -0
- package/dist/project-bootstrap-report.js +30 -0
- package/dist/project-bootstrap-report.js.map +1 -0
- package/dist/project-bootstrap-state.d.ts +87 -0
- package/dist/project-bootstrap-state.d.ts.map +1 -0
- package/dist/project-bootstrap-state.js +125 -0
- package/dist/project-bootstrap-state.js.map +1 -0
- package/dist/project-bootstrap.d.ts +36 -0
- package/dist/project-bootstrap.d.ts.map +1 -0
- package/dist/project-bootstrap.js +243 -0
- package/dist/project-bootstrap.js.map +1 -0
- package/dist/project-scaffold-templates.d.ts +1 -1
- package/dist/project-scaffold-templates.d.ts.map +1 -1
- package/dist/project-scaffold-templates.js +17 -12
- package/dist/project-scaffold-templates.js.map +1 -1
- package/dist/project-toolchain.d.ts +13 -0
- package/dist/project-toolchain.d.ts.map +1 -0
- package/dist/project-toolchain.js +73 -0
- package/dist/project-toolchain.js.map +1 -0
- package/dist/project.d.ts +2 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +40 -9
- package/dist/project.js.map +1 -1
- package/dist/widget-scaffold-template.d.ts.map +1 -1
- package/dist/widget-scaffold-template.js +5 -10
- package/dist/widget-scaffold-template.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +2 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +3 -2
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-bootstrap-authoring.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-bootstrap-pre-auth.js +26 -4
- package/node_modules/@noodle-borg/openapi-import/dist/index.js +76 -53
- package/node_modules/@noodle-borg/openapi-import/dist/request-body.js +125 -0
- package/node_modules/@noodle-borg/openapi-import/dist/request-input.js +139 -0
- package/node_modules/@noodle-borg/openapi-import/dist/response-schema.js +17 -4
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,39 +1,69 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
|
+
import { importRequestBody } from './request-body.js';
|
|
3
|
+
import { identifier, importOperationParameters } from './request-input.js';
|
|
2
4
|
import { importResponseSchema, toOutputJsonSchema, } from './response-schema.js';
|
|
3
5
|
export * from './mcp/index.js';
|
|
6
|
+
export { identifier } from './request-input.js';
|
|
4
7
|
export { importResponseSchema, MAX_RESPONSE_SCHEMA_DEPTH, MAX_RESPONSE_SCHEMA_NODES, toOutputJsonSchema, } from './response-schema.js';
|
|
5
8
|
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
9
|
+
export const MAX_OPENAPI_SOURCE_BYTES = 6 * 1024 * 1024;
|
|
6
10
|
export function parseOpenApiToIr(source, options) {
|
|
7
|
-
|
|
11
|
+
if (Buffer.byteLength(source, 'utf8') > MAX_OPENAPI_SOURCE_BYTES) {
|
|
12
|
+
throw new Error('import openapi: document exceeds the 6 MiB size limit; import a scoped API document');
|
|
13
|
+
}
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = YAML.parse(source, { maxAliasCount: 100 });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error('import openapi: invalid JSON/YAML document; repair its syntax and retry');
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(parsed))
|
|
22
|
+
throw new Error('import openapi: document must be an object');
|
|
23
|
+
const doc = parsed;
|
|
8
24
|
if (typeof doc.openapi !== 'string' || !/^3\.[01]\./.test(doc.openapi)) {
|
|
9
25
|
throw new Error('import openapi: only OpenAPI 3.0 and 3.1 documents are supported');
|
|
10
26
|
}
|
|
11
|
-
const baseUrl = options.baseUrl ??
|
|
27
|
+
const baseUrl = options.baseUrl ??
|
|
28
|
+
(Array.isArray(doc.servers)
|
|
29
|
+
? doc.servers.find((server) => isRecord(server) && typeof server.url === 'string')?.url
|
|
30
|
+
: undefined);
|
|
12
31
|
if (baseUrl === undefined)
|
|
13
32
|
throw new Error('import openapi: missing base URL; pass --base-url');
|
|
33
|
+
try {
|
|
34
|
+
const base = new URL(baseUrl);
|
|
35
|
+
if (!['https:', 'http:'].includes(base.protocol) ||
|
|
36
|
+
base.username ||
|
|
37
|
+
base.password ||
|
|
38
|
+
base.search ||
|
|
39
|
+
base.hash)
|
|
40
|
+
throw new Error();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('import openapi: use an HTTP(S) base URL without credentials, query or fragment');
|
|
44
|
+
}
|
|
14
45
|
const operations = [];
|
|
15
46
|
const warnings = [];
|
|
16
47
|
const referencedSchemes = [];
|
|
17
48
|
collectSecurityRefs(doc.security, referencedSchemes);
|
|
18
|
-
|
|
49
|
+
if (!isRecord(doc.paths))
|
|
50
|
+
throw new Error('import openapi: paths must be an object');
|
|
51
|
+
for (const [path, item] of Object.entries(doc.paths)) {
|
|
52
|
+
if (!isRecord(item) ||
|
|
53
|
+
!path.startsWith('/') ||
|
|
54
|
+
path.startsWith('//') ||
|
|
55
|
+
/[?#\p{Cc}]/u.test(path)) {
|
|
56
|
+
throw new Error('import openapi: use path-item objects with relative paths and declared query parameters');
|
|
57
|
+
}
|
|
19
58
|
for (const [method, rawOperation] of Object.entries(item)) {
|
|
20
59
|
if (!METHODS.has(method))
|
|
21
60
|
continue;
|
|
61
|
+
if (!isRecord(rawOperation))
|
|
62
|
+
throw new Error('import openapi: each supported operation must be an object');
|
|
22
63
|
const operation = rawOperation;
|
|
23
64
|
collectSecurityRefs(operation.security, referencedSchemes);
|
|
24
65
|
const name = identifier(typeof operation.operationId === 'string' ? operation.operationId : `${method}_${path}`);
|
|
25
|
-
const parameters = (operation.parameters
|
|
26
|
-
.filter((param) => param.in === 'path' || param.in === 'query')
|
|
27
|
-
.map((param) => ({
|
|
28
|
-
name: identifier(String(param.name ?? 'param')),
|
|
29
|
-
in: param.in,
|
|
30
|
-
required: param.required === true,
|
|
31
|
-
schema: parameterJsonSchema(param.schema),
|
|
32
|
-
}));
|
|
33
|
-
const hasRequestBody = operation.requestBody !== undefined;
|
|
34
|
-
if (hasRequestBody) {
|
|
35
|
-
warnings.push(`${name}: requestBody imported as opaque JSON string placeholder`);
|
|
36
|
-
}
|
|
66
|
+
const { parameters, connectorPath } = importOperationParameters(path, item.parameters, operation.parameters);
|
|
37
67
|
const safeName = uniqueName(manifestIdentifier(name), new Set(operations.map((op) => op.safeName)));
|
|
38
68
|
const responseCtx = {
|
|
39
69
|
operationName: name,
|
|
@@ -41,6 +71,13 @@ export function parseOpenApiToIr(source, options) {
|
|
|
41
71
|
warnings,
|
|
42
72
|
};
|
|
43
73
|
const output = importResponseSchema(operation.responses, responseCtx);
|
|
74
|
+
const requestBody = importRequestBody(operation.requestBody, responseCtx);
|
|
75
|
+
if (requestBody !== undefined && method === 'get') {
|
|
76
|
+
throw new Error('import openapi: GET request bodies require an explicitly authored mapping');
|
|
77
|
+
}
|
|
78
|
+
if (requestBody !== undefined && parameters.some((param) => param.name === 'body')) {
|
|
79
|
+
throw new Error('import openapi: request body and parameter input-name collision; map the inputs explicitly');
|
|
80
|
+
}
|
|
44
81
|
operations.push({
|
|
45
82
|
name,
|
|
46
83
|
safeName,
|
|
@@ -49,10 +86,10 @@ export function parseOpenApiToIr(source, options) {
|
|
|
49
86
|
path,
|
|
50
87
|
// OpenAPI's `{name}` path params are already the connector runtime's template form; rewriting
|
|
51
88
|
// them to `${args.name}` made catalog compilation reject the path (unsupported_path_expression).
|
|
52
|
-
connectorPath
|
|
89
|
+
connectorPath,
|
|
53
90
|
description: `Call ${method.toUpperCase()} ${path}.`,
|
|
54
91
|
parameters,
|
|
55
|
-
|
|
92
|
+
...(requestBody === undefined ? {} : { requestBody }),
|
|
56
93
|
query: parameters.filter((param) => param.in === 'query').map((param) => param.name),
|
|
57
94
|
...(output !== undefined ? { output } : {}),
|
|
58
95
|
});
|
|
@@ -124,7 +161,7 @@ export function renderOpenApiConnectorsYaml(ir) {
|
|
|
124
161
|
kind: 'custom',
|
|
125
162
|
http: {
|
|
126
163
|
baseUrl: ir.baseUrl,
|
|
127
|
-
allowedOrigins: [ir.baseUrl],
|
|
164
|
+
allowedOrigins: [new URL(ir.baseUrl).origin],
|
|
128
165
|
...(ir.auth !== undefined ? { auth: ir.auth } : {}),
|
|
129
166
|
},
|
|
130
167
|
operations: Object.fromEntries(ir.operations.map((operation) => [operation.safeName, connectorOperation(operation)])),
|
|
@@ -153,7 +190,7 @@ export function renderOpenApiManifestYaml(ir) {
|
|
|
153
190
|
...outputSchemaEntry(operation),
|
|
154
191
|
fulfilment: {
|
|
155
192
|
use: `api.${operation.safeName}`,
|
|
156
|
-
args:
|
|
193
|
+
args: inputArguments(operation),
|
|
157
194
|
},
|
|
158
195
|
})),
|
|
159
196
|
})}`;
|
|
@@ -220,7 +257,7 @@ export function mergeOpenApiIntoDraft(input) {
|
|
|
220
257
|
...outputSchemaEntry(operation),
|
|
221
258
|
fulfilment: {
|
|
222
259
|
use: `${connectorAlias}.${operationName}`,
|
|
223
|
-
args:
|
|
260
|
+
args: inputArguments(operation),
|
|
224
261
|
},
|
|
225
262
|
});
|
|
226
263
|
existingToolNames.add(toolName);
|
|
@@ -238,10 +275,6 @@ export function mergeOpenApiIntoDraft(input) {
|
|
|
238
275
|
warnings,
|
|
239
276
|
};
|
|
240
277
|
}
|
|
241
|
-
export function identifier(value) {
|
|
242
|
-
const out = value.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
243
|
-
return /^[A-Za-z_]/.test(out) ? out : `op_${out || 'operation'}`;
|
|
244
|
-
}
|
|
245
278
|
export function title(value) {
|
|
246
279
|
return value.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
247
280
|
}
|
|
@@ -255,17 +288,12 @@ function manifestIdentifier(value) {
|
|
|
255
288
|
return /^[a-z0-9_]+$/.test(snake) && snake.length > 0 ? snake : 'operation';
|
|
256
289
|
}
|
|
257
290
|
function connectorOperation(operation) {
|
|
258
|
-
const required = operation.parameters.filter((param) => param.required).map((p) => p.name);
|
|
259
291
|
return {
|
|
260
292
|
type: operation.operationType,
|
|
261
293
|
method: operation.method.toUpperCase(),
|
|
262
294
|
path: operation.connectorPath,
|
|
263
|
-
input:
|
|
264
|
-
|
|
265
|
-
properties: Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
|
|
266
|
-
...(required.length > 0 ? { required } : {}),
|
|
267
|
-
additionalProperties: false,
|
|
268
|
-
},
|
|
295
|
+
input: inputSchema(operation),
|
|
296
|
+
...(operation.requestBody === undefined ? {} : { request: '${args.body}' }),
|
|
269
297
|
...(operation.query.length > 0 ? { query: [...operation.query] } : {}),
|
|
270
298
|
// The same lossless `{ value: <tree> }` object the tool outputSchema advertises; an untyped
|
|
271
299
|
// operation keeps an open ({}) value subtree. Response mapping wraps the JSON body as `value`.
|
|
@@ -292,26 +320,28 @@ function outputSchemaEntry(operation) {
|
|
|
292
320
|
};
|
|
293
321
|
}
|
|
294
322
|
function inputSchema(operation) {
|
|
323
|
+
const required = operation.parameters
|
|
324
|
+
.filter((param) => param.required)
|
|
325
|
+
.map((param) => param.name);
|
|
326
|
+
if (operation.requestBody?.required)
|
|
327
|
+
required.push('body');
|
|
295
328
|
return {
|
|
296
329
|
type: 'object',
|
|
297
|
-
properties:
|
|
298
|
-
|
|
330
|
+
properties: {
|
|
331
|
+
...Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
|
|
332
|
+
...(operation.requestBody === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: { body: toOutputJsonSchema(operation.requestBody.schema) }),
|
|
335
|
+
},
|
|
336
|
+
required,
|
|
299
337
|
additionalProperties: false,
|
|
300
338
|
};
|
|
301
339
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const base = rawType === 'number' || rawType === 'integer' || rawType === 'boolean' ? rawType : 'string';
|
|
308
|
-
const enumValues = base === 'string' ? stringEnum(schema.enum) : undefined;
|
|
309
|
-
const format = typeof schema.format === 'string' ? schema.format : undefined;
|
|
310
|
-
return {
|
|
311
|
-
type: schema.nullable === true ? [base, 'null'] : base,
|
|
312
|
-
...(enumValues !== undefined ? { enum: enumValues } : {}),
|
|
313
|
-
...(format !== undefined ? { format } : {}),
|
|
314
|
-
};
|
|
340
|
+
function inputArguments(operation) {
|
|
341
|
+
return Object.fromEntries([
|
|
342
|
+
...operation.parameters.map((param) => [param.name, `\${input.${param.name}}`]),
|
|
343
|
+
...(operation.requestBody === undefined ? [] : [['body', '${input.body}']]),
|
|
344
|
+
]);
|
|
315
345
|
}
|
|
316
346
|
/** A fresh plain-JSON copy of a parameter schema, safe to embed in YAML documents. */
|
|
317
347
|
function parameterSchemaJson(schema) {
|
|
@@ -321,13 +351,6 @@ function parameterSchemaJson(schema) {
|
|
|
321
351
|
...(schema.format !== undefined ? { format: schema.format } : {}),
|
|
322
352
|
};
|
|
323
353
|
}
|
|
324
|
-
function stringEnum(value) {
|
|
325
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
326
|
-
return undefined;
|
|
327
|
-
if (!value.every((entry) => typeof entry === 'string'))
|
|
328
|
-
return undefined;
|
|
329
|
-
return value;
|
|
330
|
-
}
|
|
331
354
|
function parseYamlObject(source, label) {
|
|
332
355
|
const parsed = YAML.parse(source);
|
|
333
356
|
if (!isRecord(parsed))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { importSchemaTree, MAX_RESPONSE_SCHEMA_DEPTH, MAX_RESPONSE_SCHEMA_NODES, } from './response-schema.js';
|
|
2
|
+
/** Request inputs must not silently lose constraints or become an untyped body placeholder. */
|
|
3
|
+
export function importRequestBody(value, ctx) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (!isRecord(value) || value.$ref !== undefined) {
|
|
7
|
+
throw new Error('import openapi: inline the request body declaration before import');
|
|
8
|
+
}
|
|
9
|
+
if (value.required !== undefined && typeof value.required !== 'boolean') {
|
|
10
|
+
throw new Error('import openapi: request body required must be boolean');
|
|
11
|
+
}
|
|
12
|
+
const content = isRecord(value.content) ? value.content : undefined;
|
|
13
|
+
const media = content?.['application/json'];
|
|
14
|
+
const raw = isRecord(media) ? media.schema : undefined;
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
throw new Error('import openapi: request body must declare an application/json schema; map other encodings explicitly');
|
|
17
|
+
assertRequestSchema(raw, ctx);
|
|
18
|
+
const warningCount = ctx.warnings.length;
|
|
19
|
+
const schema = importSchemaTree(raw, ctx, true);
|
|
20
|
+
if (schema.kind === 'unknown' || ctx.warnings.length !== warningCount) {
|
|
21
|
+
throw new Error('import openapi: request body schema cannot be imported faithfully; simplify it or author its mapping explicitly');
|
|
22
|
+
}
|
|
23
|
+
return { required: value.required === true, schema };
|
|
24
|
+
}
|
|
25
|
+
function assertRequestSchema(raw, ctx) {
|
|
26
|
+
const allowed = new Set([
|
|
27
|
+
'type',
|
|
28
|
+
'nullable',
|
|
29
|
+
'properties',
|
|
30
|
+
'required',
|
|
31
|
+
'items',
|
|
32
|
+
'enum',
|
|
33
|
+
'additionalProperties',
|
|
34
|
+
'$ref',
|
|
35
|
+
'title',
|
|
36
|
+
'description',
|
|
37
|
+
'example',
|
|
38
|
+
'examples',
|
|
39
|
+
'deprecated',
|
|
40
|
+
]);
|
|
41
|
+
let nodes = 0;
|
|
42
|
+
const visit = (value, depth, refs) => {
|
|
43
|
+
nodes++;
|
|
44
|
+
if (!isRecord(value) ||
|
|
45
|
+
depth > MAX_RESPONSE_SCHEMA_DEPTH ||
|
|
46
|
+
nodes > MAX_RESPONSE_SCHEMA_NODES) {
|
|
47
|
+
throw new Error('import openapi: request body schema exceeds supported structure/depth/size bounds');
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
50
|
+
throw new Error('import openapi: request body contains an unsupported schema keyword; author its validated mapping explicitly');
|
|
51
|
+
}
|
|
52
|
+
if (value.$ref !== undefined) {
|
|
53
|
+
if (Object.keys(value).some((key) => !['$ref', 'title', 'description', 'example', 'examples', 'deprecated'].includes(key))) {
|
|
54
|
+
throw new Error('import openapi: request body reference siblings cannot be imported faithfully; inline the schema');
|
|
55
|
+
}
|
|
56
|
+
if (typeof value.$ref !== 'string' ||
|
|
57
|
+
!value.$ref.startsWith('#/components/schemas/') ||
|
|
58
|
+
refs.has(value.$ref)) {
|
|
59
|
+
throw new Error('import openapi: request body has an external, circular or unsupported reference; bundle it into an inline schema');
|
|
60
|
+
}
|
|
61
|
+
const name = value.$ref
|
|
62
|
+
.slice('#/components/schemas/'.length)
|
|
63
|
+
.replace(/~1/g, '/')
|
|
64
|
+
.replace(/~0/g, '~');
|
|
65
|
+
visit(ctx.componentSchemas[name], depth + 1, new Set([...refs, value.$ref]));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const types = Array.isArray(value.type) ? value.type : [value.type];
|
|
69
|
+
const concrete = types.filter((type) => type !== 'null');
|
|
70
|
+
const type = concrete[0];
|
|
71
|
+
if (concrete.length !== 1 ||
|
|
72
|
+
typeof type !== 'string' ||
|
|
73
|
+
!['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(type) ||
|
|
74
|
+
types.length > 2) {
|
|
75
|
+
throw new Error('import openapi: request body requires a supported explicit type at every schema node');
|
|
76
|
+
}
|
|
77
|
+
if (value.nullable !== undefined && typeof value.nullable !== 'boolean') {
|
|
78
|
+
throw new Error('import openapi: request body nullable must be boolean');
|
|
79
|
+
}
|
|
80
|
+
if ((value.properties !== undefined ||
|
|
81
|
+
value.required !== undefined ||
|
|
82
|
+
value.additionalProperties !== undefined) &&
|
|
83
|
+
type !== 'object') {
|
|
84
|
+
throw new Error('import openapi: request body object keywords require an object type');
|
|
85
|
+
}
|
|
86
|
+
if ((type === 'array' && value.items === undefined) ||
|
|
87
|
+
(value.items !== undefined && type !== 'array')) {
|
|
88
|
+
throw new Error('import openapi: request body arrays require one typed items schema');
|
|
89
|
+
}
|
|
90
|
+
if (value.additionalProperties !== undefined &&
|
|
91
|
+
typeof value.additionalProperties !== 'boolean') {
|
|
92
|
+
throw new Error('import openapi: request body additionalProperties must be boolean');
|
|
93
|
+
}
|
|
94
|
+
if (value.enum !== undefined &&
|
|
95
|
+
(!Array.isArray(value.enum) ||
|
|
96
|
+
value.enum.length === 0 ||
|
|
97
|
+
!value.enum.every((item) => typeof item === 'string'))) {
|
|
98
|
+
throw new Error('import openapi: request body supports only nonempty string enums');
|
|
99
|
+
}
|
|
100
|
+
if (value.enum !== undefined &&
|
|
101
|
+
(type !== 'string' || types.includes('null') || value.nullable === true)) {
|
|
102
|
+
throw new Error('import openapi: request body enum/nullable combination requires an explicit mapping');
|
|
103
|
+
}
|
|
104
|
+
if (value.required !== undefined &&
|
|
105
|
+
(!Array.isArray(value.required) ||
|
|
106
|
+
!value.required.every((name) => typeof name === 'string' &&
|
|
107
|
+
isRecord(value.properties) &&
|
|
108
|
+
Object.hasOwn(value.properties, name)))) {
|
|
109
|
+
throw new Error('import openapi: request body required fields must have declared properties');
|
|
110
|
+
}
|
|
111
|
+
if (value.properties !== undefined) {
|
|
112
|
+
if (!isRecord(value.properties))
|
|
113
|
+
throw new Error('import openapi: request body properties must be an object');
|
|
114
|
+
for (const child of Object.values(value.properties))
|
|
115
|
+
visit(child, depth + 1, refs);
|
|
116
|
+
}
|
|
117
|
+
if (value.items !== undefined)
|
|
118
|
+
visit(value.items, depth + 1, refs);
|
|
119
|
+
};
|
|
120
|
+
visit(raw, 0, new Set());
|
|
121
|
+
}
|
|
122
|
+
function isRecord(value) {
|
|
123
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=request-body.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
const RESERVED_FIELDS = new Set([
|
|
3
|
+
'then',
|
|
4
|
+
'equals',
|
|
5
|
+
'at',
|
|
6
|
+
'optional',
|
|
7
|
+
'toExpression',
|
|
8
|
+
'toString',
|
|
9
|
+
'__proto__',
|
|
10
|
+
'constructor',
|
|
11
|
+
'prototype',
|
|
12
|
+
]);
|
|
13
|
+
export function identifier(value) {
|
|
14
|
+
const out = value.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
15
|
+
return /^[A-Za-z_]/.test(out) ? out : `op_${out || 'operation'}`;
|
|
16
|
+
}
|
|
17
|
+
/** Keep wire query names intact; path placeholder names may be normalized with their arguments. */
|
|
18
|
+
export function importOperationParameters(path, inherited, own) {
|
|
19
|
+
const selected = new Map();
|
|
20
|
+
for (const list of [inherited, own]) {
|
|
21
|
+
if (list === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
if (!Array.isArray(list))
|
|
24
|
+
throw new Error('import openapi: parameters must be an array');
|
|
25
|
+
for (const value of list) {
|
|
26
|
+
if (!isRecord(value) ||
|
|
27
|
+
typeof value.name !== 'string' ||
|
|
28
|
+
value.name.length === 0 ||
|
|
29
|
+
value.name.length > 128 ||
|
|
30
|
+
/\p{Cc}/u.test(value.name)) {
|
|
31
|
+
throw new Error('import openapi: use inline parameters with bounded, non-control names');
|
|
32
|
+
}
|
|
33
|
+
if (value.in !== 'path' && value.in !== 'query') {
|
|
34
|
+
throw new Error('import openapi: only path/query parameters are supported; map other locations explicitly in TypeScript');
|
|
35
|
+
}
|
|
36
|
+
if (value.style !== undefined && value.style !== (value.in === 'path' ? 'simple' : 'form')) {
|
|
37
|
+
throw new Error('import openapi: unsupported parameter serialization style; map it explicitly in TypeScript');
|
|
38
|
+
}
|
|
39
|
+
if (value.allowReserved === true ||
|
|
40
|
+
value.allowEmptyValue === true ||
|
|
41
|
+
value.content !== undefined) {
|
|
42
|
+
throw new Error('import openapi: unsupported parameter wire encoding; map it explicitly in TypeScript');
|
|
43
|
+
}
|
|
44
|
+
selected.set(`${value.in}:${value.name}`, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (selected.size > 200)
|
|
48
|
+
throw new Error('import openapi: operation exceeds 200 parameters');
|
|
49
|
+
const names = new Set();
|
|
50
|
+
const pathNames = new Map();
|
|
51
|
+
const parameters = [...selected.values()].map((value) => {
|
|
52
|
+
const rawName = String(value.name);
|
|
53
|
+
let name = value.in === 'path' ? identifier(rawName) : rawName;
|
|
54
|
+
if (value.in === 'path' && RESERVED_FIELDS.has(name))
|
|
55
|
+
name = `param_${name}`;
|
|
56
|
+
if (!FIELD.test(name) || RESERVED_FIELDS.has(name)) {
|
|
57
|
+
throw new Error('import openapi: query parameter name cannot be addressed by the authoring expression contract; map this API through an application handler');
|
|
58
|
+
}
|
|
59
|
+
if (names.has(name))
|
|
60
|
+
throw new Error('import openapi: parameter input-name collision; map the inputs explicitly');
|
|
61
|
+
names.add(name);
|
|
62
|
+
if (value.in === 'path')
|
|
63
|
+
pathNames.set(rawName, name);
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
in: value.in === 'path' ? 'path' : 'query',
|
|
67
|
+
required: value.in === 'path' || value.required === true,
|
|
68
|
+
schema: parameterJsonSchema(value.schema),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const placeholders = [...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
|
|
72
|
+
if (placeholders.some((name) => name === undefined || !pathNames.has(name)) ||
|
|
73
|
+
[...pathNames.keys()].some((name) => !placeholders.includes(name))) {
|
|
74
|
+
throw new Error('import openapi: every path placeholder must match a declared path parameter');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
parameters,
|
|
78
|
+
connectorPath: path.replace(/\{([^{}]+)\}/g, (_match, name) => `{${pathNames.get(name)}}`),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parameterJsonSchema(schema) {
|
|
82
|
+
if (schema === undefined)
|
|
83
|
+
return { type: 'string' };
|
|
84
|
+
if (!isRecord(schema))
|
|
85
|
+
throw new Error('import openapi: parameter schema must be an object');
|
|
86
|
+
if (schema.$ref !== undefined)
|
|
87
|
+
throw new Error('import openapi: inline parameter schemas before import; references are not fetched');
|
|
88
|
+
const allowed = new Set([
|
|
89
|
+
'type',
|
|
90
|
+
'nullable',
|
|
91
|
+
'enum',
|
|
92
|
+
'format',
|
|
93
|
+
'title',
|
|
94
|
+
'description',
|
|
95
|
+
'example',
|
|
96
|
+
'examples',
|
|
97
|
+
'deprecated',
|
|
98
|
+
]);
|
|
99
|
+
if (Object.keys(schema).some((key) => !allowed.has(key))) {
|
|
100
|
+
throw new Error('import openapi: unsupported parameter schema constraint; author its validated mapping explicitly');
|
|
101
|
+
}
|
|
102
|
+
const types = Array.isArray(schema.type)
|
|
103
|
+
? schema.type
|
|
104
|
+
: [schema.type ?? 'string'];
|
|
105
|
+
const concrete = types.filter((type) => type !== 'null');
|
|
106
|
+
const base = concrete[0];
|
|
107
|
+
if (concrete.length !== 1 ||
|
|
108
|
+
types.length > 2 ||
|
|
109
|
+
typeof base !== 'string' ||
|
|
110
|
+
!['string', 'number', 'integer', 'boolean'].includes(base)) {
|
|
111
|
+
throw new Error('import openapi: unsupported parameter schema; use a scalar or an explicitly authored mapping');
|
|
112
|
+
}
|
|
113
|
+
const enumValues = base === 'string' ? stringEnum(schema.enum) : undefined;
|
|
114
|
+
if (schema.enum !== undefined &&
|
|
115
|
+
(enumValues === undefined || types.includes('null') || schema.nullable === true)) {
|
|
116
|
+
throw new Error('import openapi: unsupported parameter enum/nullable combination; author an explicit mapping');
|
|
117
|
+
}
|
|
118
|
+
const format = typeof schema.format === 'string' ? schema.format : undefined;
|
|
119
|
+
if ((schema.nullable !== undefined && typeof schema.nullable !== 'boolean') ||
|
|
120
|
+
(schema.format !== undefined && format === undefined)) {
|
|
121
|
+
throw new Error('import openapi: invalid parameter nullable or format declaration');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
type: schema.nullable === true || types.includes('null') ? [base, 'null'] : base,
|
|
125
|
+
...(enumValues !== undefined ? { enum: enumValues } : {}),
|
|
126
|
+
...(format !== undefined ? { format } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function stringEnum(value) {
|
|
130
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
if (!value.every((entry) => typeof entry === 'string'))
|
|
133
|
+
return undefined;
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=request-input.js.map
|
|
@@ -8,6 +8,10 @@ export const MAX_RESPONSE_SCHEMA_DEPTH = 8;
|
|
|
8
8
|
export const MAX_RESPONSE_SCHEMA_NODES = 200;
|
|
9
9
|
const UNKNOWN = { kind: 'unknown' };
|
|
10
10
|
const UNSUPPORTED_COMPOSITIONS = ['oneOf', 'anyOf', 'allOf', 'not'];
|
|
11
|
+
/** Shared bounded conversion; request validation is owned by request-body.ts. */
|
|
12
|
+
export function importSchemaTree(raw, ctx, request = false) {
|
|
13
|
+
return convertSchema(raw, ctx, { nodes: 0, boundWarned: false, request }, 0, new Set());
|
|
14
|
+
}
|
|
11
15
|
/**
|
|
12
16
|
* Picks the best 2xx `application/json` schema from an operation's `responses` block and
|
|
13
17
|
* converts it. Returns `undefined` (with warnings pushed to the context) when the operation
|
|
@@ -41,14 +45,17 @@ export function toOutputJsonSchema(schema) {
|
|
|
41
45
|
case 'string':
|
|
42
46
|
return withNullable({ type: 'string', ...(schema.enum !== undefined ? { enum: [...schema.enum] } : {}) }, schema.nullable);
|
|
43
47
|
case 'number':
|
|
44
|
-
return withNullable({ type: 'number' }, schema.nullable);
|
|
48
|
+
return withNullable({ type: schema.integer === true ? 'integer' : 'number' }, schema.nullable);
|
|
45
49
|
case 'boolean':
|
|
46
50
|
return withNullable({ type: 'boolean' }, schema.nullable);
|
|
47
51
|
case 'array':
|
|
48
52
|
return withNullable({ type: 'array', items: toOutputJsonSchema(schema.items) }, schema.nullable);
|
|
49
53
|
case 'object': {
|
|
54
|
+
const extra = schema.additionalProperties === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { additionalProperties: schema.additionalProperties };
|
|
50
57
|
if (schema.properties.length === 0)
|
|
51
|
-
return withNullable({ type: 'object' }, schema.nullable);
|
|
58
|
+
return withNullable({ type: 'object', ...extra }, schema.nullable);
|
|
52
59
|
const required = schema.properties
|
|
53
60
|
.filter((property) => property.required)
|
|
54
61
|
.map((property) => property.name);
|
|
@@ -59,6 +66,7 @@ export function toOutputJsonSchema(schema) {
|
|
|
59
66
|
toOutputJsonSchema(property.schema),
|
|
60
67
|
])),
|
|
61
68
|
...(required.length > 0 ? { required } : {}),
|
|
69
|
+
...extra,
|
|
62
70
|
}, schema.nullable);
|
|
63
71
|
}
|
|
64
72
|
case 'unknown':
|
|
@@ -101,7 +109,11 @@ function convertSchema(raw, ctx, state, depth, visitedRefs) {
|
|
|
101
109
|
}
|
|
102
110
|
case 'number':
|
|
103
111
|
case 'integer':
|
|
104
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
kind: 'number',
|
|
114
|
+
...nullableSpread,
|
|
115
|
+
...(state.request && type === 'integer' ? { integer: true } : {}),
|
|
116
|
+
};
|
|
105
117
|
case 'boolean':
|
|
106
118
|
return { kind: 'boolean', ...nullableSpread };
|
|
107
119
|
case 'array':
|
|
@@ -120,6 +132,7 @@ function convertSchema(raw, ctx, state, depth, visitedRefs) {
|
|
|
120
132
|
return {
|
|
121
133
|
kind: 'object',
|
|
122
134
|
...nullableSpread,
|
|
135
|
+
...(state.request ? { additionalProperties: raw.additionalProperties !== false } : {}),
|
|
123
136
|
properties: Object.entries(properties).map(([name, propertySchema]) => ({
|
|
124
137
|
name,
|
|
125
138
|
required: required.has(name),
|
|
@@ -143,7 +156,7 @@ function convertRef(ref, ctx, state, depth, visitedRefs) {
|
|
|
143
156
|
ctx.warnings.push(`${ctx.operationName}: response schema $ref "${ref}" is not a component schema reference; that part is left untyped`);
|
|
144
157
|
return UNKNOWN;
|
|
145
158
|
}
|
|
146
|
-
const name = ref.slice(prefix.length);
|
|
159
|
+
const name = ref.slice(prefix.length).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
147
160
|
if (visitedRefs.has(name)) {
|
|
148
161
|
ctx.warnings.push(`${ctx.operationName}: response schema $ref "${ref}" is circular; the repeated part is left untyped`);
|
|
149
162
|
return UNKNOWN;
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
41
|
"@noodle-borg/admission-limits": "0.0.0",
|
|
42
|
-
"@noodle-borg/agent-kit": "0.
|
|
42
|
+
"@noodle-borg/agent-kit": "0.96.0",
|
|
43
43
|
"@noodle-borg/app-package": "0.0.0",
|
|
44
44
|
"@noodle-borg/assistant-gateway": "0.0.0",
|
|
45
45
|
"@noodle-borg/auth": "0.0.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/one",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.159.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"@modelcontextprotocol/client": "2.0.0",
|
|
236
236
|
"@modelcontextprotocol/server": "2.0.0",
|
|
237
237
|
"@noodle-borg/admission-limits": "0.0.0",
|
|
238
|
-
"@noodle-borg/agent-kit": "0.
|
|
238
|
+
"@noodle-borg/agent-kit": "0.96.0",
|
|
239
239
|
"@noodle-borg/app-audit": "0.0.0",
|
|
240
240
|
"@noodle-borg/app-package": "0.0.0",
|
|
241
241
|
"@noodle-borg/assistant-gateway": "0.0.0",
|