@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.
- package/dist/commands/deploy-first-flow.d.ts +1 -1
- package/dist/commands/deploy-first-flow.d.ts.map +1 -1
- package/dist/commands/deploy-first-flow.js +4 -193
- package/dist/commands/deploy-first-flow.js.map +1 -1
- package/dist/commands/deploy-ops.d.ts.map +1 -1
- package/dist/commands/deploy-ops.js +25 -1
- package/dist/commands/deploy-ops.js.map +1 -1
- package/dist/commands/deploy-preflight-command.d.ts +8 -0
- package/dist/commands/deploy-preflight-command.d.ts.map +1 -0
- package/dist/commands/deploy-preflight-command.js +78 -0
- package/dist/commands/deploy-preflight-command.js.map +1 -0
- package/dist/commands/deploy-preflight-output.d.ts +18 -0
- package/dist/commands/deploy-preflight-output.d.ts.map +1 -0
- package/dist/commands/deploy-preflight-output.js +176 -0
- package/dist/commands/deploy-preflight-output.js.map +1 -0
- package/dist/commands/deploy-preflight.d.ts +6 -1
- package/dist/commands/deploy-preflight.d.ts.map +1 -1
- package/dist/commands/deploy-preflight.js +24 -0
- package/dist/commands/deploy-preflight.js.map +1 -1
- package/dist/commands/deploy-target.d.ts +1 -0
- package/dist/commands/deploy-target.d.ts.map +1 -1
- package/dist/commands/deploy-target.js +9 -8
- package/dist/commands/deploy-target.js.map +1 -1
- package/dist/openapi-import.d.ts.map +1 -1
- package/dist/openapi-import.js +51 -23
- package/dist/openapi-import.js.map +1 -1
- package/dist/plugin-mode/build-readiness-recorder.d.ts.map +1 -1
- package/dist/plugin-mode/build-readiness-recorder.js +3 -0
- package/dist/plugin-mode/build-readiness-recorder.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts +120 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.js +32 -1
- 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 +47 -2
- package/dist/plugin-mode/plugin-operation-tools.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.js +2 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +1 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-deployment.js +85 -68
- package/node_modules/@noodle-borg/control-plane/dist/contracts.d.ts +5 -0
- package/node_modules/@noodle-borg/control-plane/dist/contracts.js +9 -0
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.d.ts +16 -1
- package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.js +18 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
- 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/dist/routes/org-admin.js +21 -11
- package/node_modules/@noodle-borg/service/package.json +1 -1
- 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
summary: '
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|