@noodleseed/one 0.158.0 → 0.160.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.
Files changed (57) hide show
  1. package/dist/commands/deploy-first-flow.d.ts +1 -1
  2. package/dist/commands/deploy-first-flow.d.ts.map +1 -1
  3. package/dist/commands/deploy-first-flow.js +4 -193
  4. package/dist/commands/deploy-first-flow.js.map +1 -1
  5. package/dist/commands/deploy-ops.d.ts.map +1 -1
  6. package/dist/commands/deploy-ops.js +25 -1
  7. package/dist/commands/deploy-ops.js.map +1 -1
  8. package/dist/commands/deploy-preflight-command.d.ts +8 -0
  9. package/dist/commands/deploy-preflight-command.d.ts.map +1 -0
  10. package/dist/commands/deploy-preflight-command.js +78 -0
  11. package/dist/commands/deploy-preflight-command.js.map +1 -0
  12. package/dist/commands/deploy-preflight-output.d.ts +18 -0
  13. package/dist/commands/deploy-preflight-output.d.ts.map +1 -0
  14. package/dist/commands/deploy-preflight-output.js +176 -0
  15. package/dist/commands/deploy-preflight-output.js.map +1 -0
  16. package/dist/commands/deploy-preflight.d.ts +6 -1
  17. package/dist/commands/deploy-preflight.d.ts.map +1 -1
  18. package/dist/commands/deploy-preflight.js +24 -0
  19. package/dist/commands/deploy-preflight.js.map +1 -1
  20. package/dist/commands/deploy-target.d.ts +1 -0
  21. package/dist/commands/deploy-target.d.ts.map +1 -1
  22. package/dist/commands/deploy-target.js +9 -8
  23. package/dist/commands/deploy-target.js.map +1 -1
  24. package/dist/openapi-import.d.ts.map +1 -1
  25. package/dist/openapi-import.js +51 -23
  26. package/dist/openapi-import.js.map +1 -1
  27. package/dist/plugin-mode/build-readiness-recorder.d.ts.map +1 -1
  28. package/dist/plugin-mode/build-readiness-recorder.js +3 -0
  29. package/dist/plugin-mode/build-readiness-recorder.js.map +1 -1
  30. package/dist/plugin-mode/plugin-operation-contract.d.ts +120 -1
  31. package/dist/plugin-mode/plugin-operation-contract.d.ts.map +1 -1
  32. package/dist/plugin-mode/plugin-operation-contract.js +32 -1
  33. package/dist/plugin-mode/plugin-operation-contract.js.map +1 -1
  34. package/dist/plugin-mode/plugin-operation-tools.d.ts.map +1 -1
  35. package/dist/plugin-mode/plugin-operation-tools.js +47 -2
  36. package/dist/plugin-mode/plugin-operation-tools.js.map +1 -1
  37. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  38. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +1 -1
  39. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
  40. package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.js +2 -1
  41. package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js +1 -1
  42. package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +1 -0
  43. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  44. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-deployment.js +85 -68
  45. package/node_modules/@noodle-borg/control-plane/dist/contracts.d.ts +5 -0
  46. package/node_modules/@noodle-borg/control-plane/dist/contracts.js +9 -0
  47. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.d.ts +16 -1
  48. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.js +18 -0
  49. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
  50. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  51. package/node_modules/@noodle-borg/openapi-import/dist/index.js +76 -53
  52. package/node_modules/@noodle-borg/openapi-import/dist/request-body.js +125 -0
  53. package/node_modules/@noodle-borg/openapi-import/dist/request-input.js +139 -0
  54. package/node_modules/@noodle-borg/openapi-import/dist/response-schema.js +17 -4
  55. package/node_modules/@noodle-borg/service/dist/routes/org-admin.js +21 -11
  56. package/node_modules/@noodle-borg/service/package.json +1 -1
  57. package/package.json +2 -2
@@ -92,81 +92,98 @@ const WATCH_FLAGS = [
92
92
  constraints: { default: 5, minimum: 2 },
93
93
  }),
94
94
  ];
95
+ const DEPLOY_ARGUMENTS = [
96
+ argument({
97
+ ...ARGUMENT_FIELDS,
98
+ name: 'server.ts',
99
+ type: 'string',
100
+ summary: 'Server entrypoint; defaults to the linked or conventional project entrypoint.',
101
+ required: false,
102
+ }),
103
+ ];
104
+ const DEPLOY_FLAGS = [
105
+ ORG_FLAG,
106
+ APP_FLAG,
107
+ ENV_FLAG,
108
+ flag({
109
+ ...FLAG_FIELDS,
110
+ name: 'version',
111
+ type: 'string',
112
+ value: '<version>',
113
+ summary: 'Publish or overwrite an exact server version. Omit it and deploy reuses this project’s last version, a vN/ entrypoint folder, or the version the hosted app already runs — a brand-new app starts at 1.',
114
+ }),
115
+ flag({
116
+ ...FLAG_FIELDS,
117
+ name: 'private',
118
+ type: 'boolean',
119
+ summary: 'Shorthand for --access owner-only.',
120
+ }),
121
+ flag({
122
+ ...FLAG_FIELDS,
123
+ name: 'access',
124
+ type: 'string',
125
+ value: '<mode>',
126
+ summary: 'Identity access mode for the deployment.',
127
+ constraints: { choices: ACCESS_MODES },
128
+ }),
129
+ flag({
130
+ ...FLAG_FIELDS,
131
+ name: 'owner-subject',
132
+ type: 'string',
133
+ value: '<subject>',
134
+ summary: 'Exact OAuth subject bound when access is owner-only.',
135
+ }),
136
+ flag({
137
+ ...FLAG_FIELDS,
138
+ name: 'connectors',
139
+ type: 'string',
140
+ value: '<file>',
141
+ summary: 'Connector catalog override file.',
142
+ }),
143
+ SERVICE_FLAG,
144
+ AUTH_TOKEN_FLAG,
145
+ flag({
146
+ ...FLAG_FIELDS,
147
+ name: 'save',
148
+ type: 'boolean',
149
+ summary: 'Also append to ~/.noodle/servers.json.',
150
+ }),
151
+ flag({
152
+ ...FLAG_FIELDS,
153
+ name: 'no-save',
154
+ type: 'boolean',
155
+ summary: 'Do not persist deployment metadata.',
156
+ }),
157
+ flag({
158
+ ...FLAG_FIELDS,
159
+ name: 'no-prompt',
160
+ type: 'boolean',
161
+ summary: 'Never prompt to set missing variables or secrets; return the complete checklist.',
162
+ }),
163
+ JSON_FLAG,
164
+ ];
95
165
  export const CATALOG_HOSTED_DEPLOYMENT = [
96
166
  {
97
167
  name: 'deploy',
98
168
  section: 'start',
169
+ usage: '[<server.ts>] | preflight [<server.ts>]',
99
170
  next: ['noodle login then noodle deploy', 'noodle github connect'],
100
171
  summary: 'Preflight required config, deploy your server, and verify its governed MCP URL is ready.',
101
- arguments: [
102
- argument({
103
- ...ARGUMENT_FIELDS,
104
- name: 'server.ts',
105
- type: 'string',
106
- summary: 'Server entrypoint; defaults to the linked or conventional project entrypoint.',
107
- required: false,
108
- }),
109
- ],
110
- flags: [
111
- ORG_FLAG,
112
- APP_FLAG,
113
- ENV_FLAG,
114
- flag({
115
- ...FLAG_FIELDS,
116
- name: 'version',
117
- type: 'string',
118
- value: '<version>',
119
- summary: 'Publish or overwrite an exact server version. Omit it and deploy reuses this project’s last version, a vN/ entrypoint folder, or the version the hosted app already runs — a brand-new app starts at 1.',
120
- }),
121
- flag({
122
- ...FLAG_FIELDS,
123
- name: 'private',
124
- type: 'boolean',
125
- summary: 'Shorthand for --access owner-only.',
126
- }),
127
- flag({
128
- ...FLAG_FIELDS,
129
- name: 'access',
130
- type: 'string',
131
- value: '<mode>',
132
- summary: 'Identity access mode for the deployment.',
133
- constraints: { choices: ACCESS_MODES },
134
- }),
135
- flag({
136
- ...FLAG_FIELDS,
137
- name: 'owner-subject',
138
- type: 'string',
139
- value: '<subject>',
140
- summary: 'Exact OAuth subject bound when access is owner-only.',
141
- }),
142
- flag({
143
- ...FLAG_FIELDS,
144
- name: 'connectors',
145
- type: 'string',
146
- value: '<file>',
147
- summary: 'Connector catalog override file.',
148
- }),
149
- SERVICE_FLAG,
150
- AUTH_TOKEN_FLAG,
151
- flag({
152
- ...FLAG_FIELDS,
153
- name: 'save',
154
- type: 'boolean',
155
- summary: 'Also append to ~/.noodle/servers.json.',
156
- }),
157
- flag({
158
- ...FLAG_FIELDS,
159
- name: 'no-save',
160
- type: 'boolean',
161
- summary: 'Do not persist deployment metadata.',
162
- }),
163
- flag({
164
- ...FLAG_FIELDS,
165
- name: 'no-prompt',
166
- type: 'boolean',
167
- summary: 'Never prompt to set missing variables or secrets; return the complete checklist.',
172
+ arguments: DEPLOY_ARGUMENTS,
173
+ flags: DEPLOY_FLAGS,
174
+ subcommands: [
175
+ subcommand({
176
+ name: 'preflight',
177
+ summary: 'Check the authored deployment and all reported prerequisites without configuring or publishing.',
178
+ arguments: DEPLOY_ARGUMENTS,
179
+ flags: DEPLOY_FLAGS.filter((item) => item.name !== 'save' && item.name !== 'no-save').map((item) => item.name === 'version'
180
+ ? {
181
+ ...item,
182
+ summary: 'Server version to validate without publishing. Omit it to use the normal deployment version resolution.',
183
+ }
184
+ : item),
185
+ jsonOutput: { mode: 'single' },
168
186
  }),
169
- JSON_FLAG,
170
187
  ],
171
188
  jsonOutput: { mode: 'single' },
172
189
  },
@@ -1,4 +1,9 @@
1
1
  export type OrgRole = 'owner' | 'developer';
2
+ /** The canonical principal bound to a personal workspace must remain that organization's owner. */
3
+ export declare class PersonalWorkspaceOwnerMutationError extends Error {
4
+ readonly code = "personal_workspace_owner_immutable";
5
+ constructor();
6
+ }
2
7
  export interface OrgRecord {
3
8
  readonly slug: string;
4
9
  readonly displayName?: string;
@@ -0,0 +1,9 @@
1
+ /** The canonical principal bound to a personal workspace must remain that organization's owner. */
2
+ export class PersonalWorkspaceOwnerMutationError extends Error {
3
+ code = 'personal_workspace_owner_immutable';
4
+ constructor() {
5
+ super('personal workspace owner is immutable');
6
+ this.name = 'PersonalWorkspaceOwnerMutationError';
7
+ }
8
+ }
9
+ //# sourceMappingURL=contracts.js.map
@@ -1,4 +1,4 @@
1
- import type { CreateOrgWithOwnerInput, OrgRecord, PersonalWorkspaceProvisionInput, PersonalWorkspaceProvisionResult, WelcomeEmailRecord } from './contracts.js';
1
+ import type { CreateOrgWithOwnerInput, OrgMemberRecord, OrgRecord, OrgRole, PersonalWorkspaceProvisionInput, PersonalWorkspaceProvisionResult, WelcomeEmailRecord } from './contracts.js';
2
2
  import { InMemoryOrganizationStore } from './in-memory-organization-store.js';
3
3
  /** Portable organization state. Commercial account provisioning is an optional module side effect. */
4
4
  export declare class InMemoryControlPlaneStore extends InMemoryOrganizationStore {
@@ -8,6 +8,21 @@ export declare class InMemoryControlPlaneStore extends InMemoryOrganizationStore
8
8
  });
9
9
  provisionPersonalWorkspace(input: PersonalWorkspaceProvisionInput): Promise<PersonalWorkspaceProvisionResult>;
10
10
  createOrgWithOwner(input: CreateOrgWithOwnerInput): Promise<OrgRecord>;
11
+ addOrgMember(input: {
12
+ org: string;
13
+ subject: string;
14
+ email: string;
15
+ role: OrgRole;
16
+ }): Promise<OrgMemberRecord>;
17
+ removeOrgMember(input: {
18
+ org: string;
19
+ subject: string;
20
+ }): Promise<boolean>;
21
+ updateOrgMemberRole(input: {
22
+ org: string;
23
+ subject: string;
24
+ role: OrgRole;
25
+ }): Promise<OrgMemberRecord | undefined>;
11
26
  claimWelcomeEmail(input: {
12
27
  readonly now: Date;
13
28
  readonly leaseMs: number;
@@ -1,3 +1,4 @@
1
+ import { PersonalWorkspaceOwnerMutationError } from './contracts.js';
1
2
  import { InMemoryOrganizationStore } from './in-memory-organization-store.js';
2
3
  import { personalOrgSlugSuffix } from './organization-helpers.js';
3
4
  import { validateUserOwnedOrgSlug } from './validation.js';
@@ -75,6 +76,18 @@ export class InMemoryControlPlaneStore extends InMemoryOrganizationStore {
75
76
  return org;
76
77
  });
77
78
  }
79
+ addOrgMember(input) {
80
+ this.#assertPersonalOwnerMutation(input.org, input.subject, input.role);
81
+ return super.addOrgMember(input);
82
+ }
83
+ removeOrgMember(input) {
84
+ this.#assertPersonalOwnerMutation(input.org, input.subject);
85
+ return super.removeOrgMember(input);
86
+ }
87
+ updateOrgMemberRole(input) {
88
+ this.#assertPersonalOwnerMutation(input.org, input.subject, input.role);
89
+ return super.updateOrgMemberRole(input);
90
+ }
78
91
  claimWelcomeEmail(input) {
79
92
  const nowMs = input.now.getTime();
80
93
  const record = [...this.#welcomeEmails.values()]
@@ -131,6 +144,11 @@ export class InMemoryControlPlaneStore extends InMemoryOrganizationStore {
131
144
  role: 'owner',
132
145
  });
133
146
  }
147
+ #assertPersonalOwnerMutation(org, subject, role) {
148
+ if (this.#personalWorkspaces.get(subject) === org && role !== 'owner') {
149
+ throw new PersonalWorkspaceOwnerMutationError();
150
+ }
151
+ }
134
152
  async #serialized(key, operation) {
135
153
  const prior = this.#queues.get(key) ?? Promise.resolve();
136
154
  let release;
@@ -1,5 +1,6 @@
1
1
  export { type AppPurgeReconciliationActor, type AppPurgeReconciliationConflictCode, AppPurgeReconciliationError, type AppPurgeReconciliationOperator, } from './app-purge-reconciliation-port.js';
2
2
  export type { ActiveMcpSubdomainClaim, ChangeMcpSubdomainInput, ControlPlaneIdentity, CreateOrgWithOwnerInput, McpSubdomainMutationResult, McpSubdomainSetting, OrganizationStore, OrgDomainRecord, OrgInvitationRecord, OrgMemberRecord, OrgOpenAIAppsChallengeRecord, OrgRecord, OrgRole, PersonalWorkspaceProvisionInput, PersonalWorkspaceProvisionResult, SignupAllowlistKind, SignupAllowlistRecord, WelcomeEmailRecord, } from './contracts.js';
3
+ export { PersonalWorkspaceOwnerMutationError } from './contracts.js';
3
4
  export { allowAllGate, bearerToken, type ControlPlaneAuthResult, type DeployAuthGate, } from './deploy-auth.js';
4
5
  export { CompositeControlPlaneGate, type ControlPlaneSignupMode, GoogleControlPlaneGate, type GoogleControlPlaneGateOptions, type GoogleIdTokenVerifier, GoogleWorkloadControlPlaneGate, type GoogleWorkloadControlPlaneGateOptions, NoodleOAuthControlPlaneGate, type NoodleOAuthControlPlaneGateOptions, type SignupAuthorizer, } from './deploy-gates.js';
5
6
  export { InMemoryControlPlaneStore } from './in-memory-control-plane-store.js';
@@ -1,4 +1,5 @@
1
1
  export { AppPurgeReconciliationError, } from './app-purge-reconciliation-port.js';
2
+ export { PersonalWorkspaceOwnerMutationError } from './contracts.js';
2
3
  export { allowAllGate, bearerToken, } from './deploy-auth.js';
3
4
  export { CompositeControlPlaneGate, GoogleControlPlaneGate, GoogleWorkloadControlPlaneGate, NoodleOAuthControlPlaneGate, } from './deploy-gates.js';
4
5
  export { InMemoryControlPlaneStore } from './in-memory-control-plane-store.js';
@@ -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
- const doc = YAML.parse(source);
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 ?? doc.servers?.find((server) => typeof server.url === 'string')?.url;
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
- for (const [path, item] of Object.entries(doc.paths ?? {})) {
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: path,
89
+ connectorPath,
53
90
  description: `Call ${method.toUpperCase()} ${path}.`,
54
91
  parameters,
55
- hasRequestBody,
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: Object.fromEntries(operation.parameters.map((param) => [param.name, `\${input.${param.name}}`])),
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: Object.fromEntries(operation.parameters.map((param) => [param.name, `\${input.${param.name}}`])),
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
- type: 'object',
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: Object.fromEntries(operation.parameters.map((param) => [param.name, parameterSchemaJson(param.schema)])),
298
- required: operation.parameters.filter((param) => param.required).map((param) => param.name),
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
- /** Import one parameter's OpenAPI schema as a lossless JSON Schema fragment (see the type doc). */
303
- function parameterJsonSchema(schema) {
304
- if (!isRecord(schema))
305
- return { type: 'string' };
306
- const rawType = schema.type;
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