@oneentry/mcp-platform-server 0.1.0 → 0.1.3
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/LICENSE +21 -0
- package/README.md +135 -92
- package/data/permissions.json +411 -0
- package/dist/api/audit.d.ts +0 -11
- package/dist/api/audit.js +0 -10
- package/dist/api/auth.d.ts +0 -18
- package/dist/api/auth.js +0 -20
- package/dist/api/build-catalog.d.ts +10 -0
- package/dist/api/build-catalog.js +187 -0
- package/dist/api/catalog.d.ts +8 -15
- package/dist/api/catalog.js +54 -16
- package/dist/api/client.d.ts +0 -24
- package/dist/api/client.js +6 -28
- package/dist/api/normalize-schema.d.ts +3 -0
- package/dist/api/normalize-schema.js +133 -0
- package/dist/api/policy.d.ts +0 -23
- package/dist/api/policy.js +0 -0
- package/dist/api/shape.d.ts +0 -13
- package/dist/api/shape.js +0 -21
- package/dist/api/swagger-source.d.ts +11 -0
- package/dist/api/swagger-source.js +58 -0
- package/dist/api/types.d.ts +0 -34
- package/dist/api/types.js +0 -1
- package/dist/bin/cli.d.ts +0 -1
- package/dist/bin/cli.js +11 -7
- package/dist/config/config.d.ts +66 -46
- package/dist/config/config.js +47 -44
- package/dist/index.d.ts +9 -7
- package/dist/index.js +5 -6
- package/dist/knowledge/chunk.d.ts +0 -15
- package/dist/knowledge/chunk.js +0 -16
- package/dist/knowledge/github.d.ts +21 -0
- package/dist/knowledge/github.js +174 -0
- package/dist/knowledge/loader.d.ts +23 -24
- package/dist/knowledge/loader.js +69 -108
- package/dist/knowledge/search.d.ts +1 -12
- package/dist/knowledge/search.js +19 -20
- package/dist/knowledge/tar.d.ts +7 -0
- package/dist/knowledge/tar.js +87 -0
- package/dist/knowledge/types.d.ts +8 -22
- package/dist/knowledge/types.js +0 -1
- package/dist/server.d.ts +1 -12
- package/dist/server.js +21 -18
- package/dist/session.d.ts +1 -18
- package/dist/session.js +3 -13
- package/dist/tools/api-call.d.ts +0 -2
- package/dist/tools/api-call.js +0 -23
- package/dist/tools/api-discovery.d.ts +0 -2
- package/dist/tools/api-discovery.js +2 -5
- package/dist/tools/docs.d.ts +0 -2
- package/dist/tools/docs.js +1 -5
- package/dist/tools/guide.d.ts +0 -7
- package/dist/tools/guide.js +11 -11
- package/dist/tools/result.d.ts +0 -8
- package/dist/tools/result.js +0 -7
- package/dist/tools/whoami.d.ts +0 -2
- package/dist/tools/whoami.js +5 -3
- package/dist/transports/http.d.ts +0 -6
- package/dist/transports/http.js +17 -18
- package/dist/transports/stdio.d.ts +0 -6
- package/dist/transports/stdio.js +1 -7
- package/knowledge/operating-rules.md +121 -0
- package/package.json +20 -7
- package/data/catalog.json +0 -1
- package/data/knowledge.json +0 -1
- package/dist/api/audit.d.ts.map +0 -1
- package/dist/api/audit.js.map +0 -1
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js.map +0 -1
- package/dist/api/catalog.d.ts.map +0 -1
- package/dist/api/catalog.js.map +0 -1
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js.map +0 -1
- package/dist/api/policy.d.ts.map +0 -1
- package/dist/api/policy.js.map +0 -1
- package/dist/api/shape.d.ts.map +0 -1
- package/dist/api/shape.js.map +0 -1
- package/dist/api/types.d.ts.map +0 -1
- package/dist/api/types.js.map +0 -1
- package/dist/bin/cli.d.ts.map +0 -1
- package/dist/bin/cli.js.map +0 -1
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/knowledge/chunk.d.ts.map +0 -1
- package/dist/knowledge/chunk.js.map +0 -1
- package/dist/knowledge/loader.d.ts.map +0 -1
- package/dist/knowledge/loader.js.map +0 -1
- package/dist/knowledge/search.d.ts.map +0 -1
- package/dist/knowledge/search.js.map +0 -1
- package/dist/knowledge/types.d.ts.map +0 -1
- package/dist/knowledge/types.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/tools/api-call.d.ts.map +0 -1
- package/dist/tools/api-call.js.map +0 -1
- package/dist/tools/api-discovery.d.ts.map +0 -1
- package/dist/tools/api-discovery.js.map +0 -1
- package/dist/tools/docs.d.ts.map +0 -1
- package/dist/tools/docs.js.map +0 -1
- package/dist/tools/guide.d.ts.map +0 -1
- package/dist/tools/guide.js.map +0 -1
- package/dist/tools/result.d.ts.map +0 -1
- package/dist/tools/result.js.map +0 -1
- package/dist/tools/whoami.d.ts.map +0 -1
- package/dist/tools/whoami.js.map +0 -1
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/stdio.d.ts.map +0 -1
- package/dist/transports/stdio.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { capSchema, normalizeSchema } from './normalize-schema.js';
|
|
3
|
+
const API_PREFIX = '/api/admin';
|
|
4
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
5
|
+
const ALWAYS_CONFIRM = [
|
|
6
|
+
/^\/immutable-settings/,
|
|
7
|
+
/^\/admins/,
|
|
8
|
+
/^\/backups/,
|
|
9
|
+
/^\/modules/,
|
|
10
|
+
/^\/payments\/webhook/,
|
|
11
|
+
/^\/auth\/logout\/all-users/,
|
|
12
|
+
/^\/settings-general/,
|
|
13
|
+
/^\/system\/captcha-keys/,
|
|
14
|
+
];
|
|
15
|
+
const riskOf = (method) => {
|
|
16
|
+
if (method === 'get') {
|
|
17
|
+
return 'read';
|
|
18
|
+
}
|
|
19
|
+
return method === 'delete' ? 'destructive' : 'write';
|
|
20
|
+
};
|
|
21
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
const asString = (value) => typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
23
|
+
const responseSummary = (responses) => {
|
|
24
|
+
if (!isRecord(responses)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
for (const code of ['200', '201', '202']) {
|
|
28
|
+
const entry = responses[code];
|
|
29
|
+
if (isRecord(entry)) {
|
|
30
|
+
const description = asString(entry['description']);
|
|
31
|
+
if (description) {
|
|
32
|
+
return description.replace(/\s+/g, ' ').slice(0, 300);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
const buildParams = (raw, components) => {
|
|
39
|
+
if (!Array.isArray(raw)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const params = [];
|
|
43
|
+
for (const item of raw) {
|
|
44
|
+
if (!isRecord(item)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const location = asString(item['in']);
|
|
48
|
+
const name = asString(item['name']);
|
|
49
|
+
if (!name || (location !== 'path' && location !== 'query' && location !== 'header')) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
params.push({
|
|
53
|
+
name,
|
|
54
|
+
location,
|
|
55
|
+
required: item['required'] === true || location === 'path',
|
|
56
|
+
description: asString(item['description']),
|
|
57
|
+
schema: normalizeSchema(item['schema'], components),
|
|
58
|
+
example: item['example'],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return params;
|
|
62
|
+
};
|
|
63
|
+
const buildBody = (raw, components) => {
|
|
64
|
+
if (!isRecord(raw)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const content = raw['content'];
|
|
68
|
+
if (!isRecord(content)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const contentType = Object.keys(content)[0];
|
|
72
|
+
if (!contentType) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const media = content[contentType];
|
|
76
|
+
if (!isRecord(media)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
contentType,
|
|
81
|
+
schema: capSchema(normalizeSchema(media['schema'], components)),
|
|
82
|
+
required: raw['required'] === true,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export const buildCatalog = (params) => {
|
|
86
|
+
const { rawSwagger } = params;
|
|
87
|
+
const swagger = JSON.parse(rawSwagger);
|
|
88
|
+
if (!isRecord(swagger) || !isRecord(swagger['paths'])) {
|
|
89
|
+
throw new Error('The fetched document does not look like an OpenAPI document');
|
|
90
|
+
}
|
|
91
|
+
const componentsNode = swagger['components'];
|
|
92
|
+
const components = isRecord(componentsNode) && isRecord(componentsNode['schemas'])
|
|
93
|
+
? componentsNode['schemas']
|
|
94
|
+
: {};
|
|
95
|
+
const { byOpId, permissions } = params.permissions;
|
|
96
|
+
const operations = [];
|
|
97
|
+
const foreignPaths = [];
|
|
98
|
+
const withoutOpId = [];
|
|
99
|
+
for (const [fullPath, pathItem] of Object.entries(swagger['paths'])) {
|
|
100
|
+
if (!isRecord(pathItem)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!fullPath.startsWith(API_PREFIX)) {
|
|
104
|
+
foreignPaths.push(fullPath);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const path = fullPath.slice(API_PREFIX.length) || '/';
|
|
108
|
+
for (const method of METHODS) {
|
|
109
|
+
const operation = pathItem[method];
|
|
110
|
+
if (!isRecord(operation)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const opId = asString(operation['operationId']);
|
|
114
|
+
if (!opId) {
|
|
115
|
+
withoutOpId.push(`${method.toUpperCase()} ${fullPath}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const tags = operation['tags'];
|
|
119
|
+
const tag = (Array.isArray(tags) ? asString(tags[0]) : undefined) ?? 'Other';
|
|
120
|
+
const summary = asString(operation['summary']) ?? asString(operation['description']) ?? '';
|
|
121
|
+
const risk = riskOf(method);
|
|
122
|
+
const alwaysConfirm = method !== 'get' && ALWAYS_CONFIRM.some((pattern) => pattern.test(path));
|
|
123
|
+
const permission = byOpId[opId];
|
|
124
|
+
const entry = {
|
|
125
|
+
opId,
|
|
126
|
+
method,
|
|
127
|
+
path,
|
|
128
|
+
fullPath,
|
|
129
|
+
tag,
|
|
130
|
+
summary,
|
|
131
|
+
...(permission ? { permission } : {}),
|
|
132
|
+
risk,
|
|
133
|
+
alwaysConfirm,
|
|
134
|
+
params: buildParams(operation['parameters'], components),
|
|
135
|
+
...(buildBody(operation['requestBody'], components)
|
|
136
|
+
? { body: buildBody(operation['requestBody'], components) }
|
|
137
|
+
: {}),
|
|
138
|
+
...(responseSummary(operation['responses'])
|
|
139
|
+
? { responseSummary: responseSummary(operation['responses']) }
|
|
140
|
+
: {}),
|
|
141
|
+
searchText: '',
|
|
142
|
+
};
|
|
143
|
+
entry.searchText = [opId, method.toUpperCase(), path, tag, summary, permission ?? '']
|
|
144
|
+
.join(' ')
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
operations.push(entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
operations.sort((a, b) => a.opId.localeCompare(b.opId));
|
|
150
|
+
const warnings = [];
|
|
151
|
+
if (foreignPaths.length > 0) {
|
|
152
|
+
warnings.push(`${String(foreignPaths.length)} path(s) outside ${API_PREFIX} were dropped from the catalog ` +
|
|
153
|
+
`(e.g. ${foreignPaths.slice(0, 3).join(', ')}). This server exposes the Admin API only.`);
|
|
154
|
+
}
|
|
155
|
+
if (withoutOpId.length > 0) {
|
|
156
|
+
warnings.push(`${String(withoutOpId.length)} operation(s) have no operationId and cannot be called ` +
|
|
157
|
+
`(e.g. ${withoutOpId.slice(0, 3).join(', ')}). Report it against the CMS.`);
|
|
158
|
+
}
|
|
159
|
+
const known = new Set(operations.map((o) => o.opId));
|
|
160
|
+
const missing = Object.keys(byOpId).filter((opId) => !known.has(opId) && !opId.startsWith('Developer') && !opId.startsWith('Content'));
|
|
161
|
+
if (missing.length > 0) {
|
|
162
|
+
warnings.push(`${String(missing.length)} admin operation(s) declare a permission but are absent from the ` +
|
|
163
|
+
`instance's API document (e.g. ${missing.slice(0, 3).join(', ')}). The bundled permission ` +
|
|
164
|
+
'map was generated from a different platform revision than this instance runs — update ' +
|
|
165
|
+
'the package if the catalog looks incomplete.');
|
|
166
|
+
}
|
|
167
|
+
const knownPermissions = new Set(permissions);
|
|
168
|
+
const orphanPermissions = [
|
|
169
|
+
...new Set(operations
|
|
170
|
+
.map((operation) => operation.permission)
|
|
171
|
+
.filter((value) => value !== undefined && !knownPermissions.has(value))),
|
|
172
|
+
];
|
|
173
|
+
if (orphanPermissions.length > 0) {
|
|
174
|
+
warnings.push(`${orphanPermissions.length} permission(s) are required by operations but cannot be held by ` +
|
|
175
|
+
`any admin (${orphanPermissions.join(', ')}). Those operations answer 403 permanently — ` +
|
|
176
|
+
'treat them as unavailable and report it rather than retrying.');
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
version: 1,
|
|
180
|
+
builtAt: params.builtAt,
|
|
181
|
+
apiPrefix: API_PREFIX,
|
|
182
|
+
swaggerHash: createHash('sha256').update(rawSwagger).digest('hex').slice(0, 16),
|
|
183
|
+
permissions,
|
|
184
|
+
warnings,
|
|
185
|
+
operations,
|
|
186
|
+
};
|
|
187
|
+
};
|
package/dist/api/catalog.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { Config } from '../config/config.js';
|
|
2
|
+
import { type PermissionMap } from './build-catalog.js';
|
|
1
3
|
import type { Catalog, Operation } from './types.js';
|
|
2
|
-
/** Найденная операция с оценкой релевантности. */
|
|
3
4
|
export interface OperationHit {
|
|
4
5
|
opId: string;
|
|
5
6
|
method: string;
|
|
@@ -10,23 +11,16 @@ export interface OperationHit {
|
|
|
10
11
|
risk: Operation['risk'];
|
|
11
12
|
score: number;
|
|
12
13
|
}
|
|
13
|
-
/**
|
|
14
|
-
* Индекс каталога операций.
|
|
15
|
-
* Поиск здесь примитивный — подсчёт совпадений термов по склеенной строке.
|
|
16
|
-
* Это осознанно: путей 478, они короткие и хорошо самоописаны,
|
|
17
|
-
* полнотекстовый движок дал бы ту же выдачу при большем весе.
|
|
18
|
-
*/
|
|
19
14
|
export declare class OperationCatalog {
|
|
20
|
-
private
|
|
21
|
-
|
|
15
|
+
private byOpId;
|
|
16
|
+
catalog: Catalog;
|
|
22
17
|
constructor(catalog: Catalog);
|
|
23
|
-
static
|
|
18
|
+
static permissionMap(): PermissionMap;
|
|
19
|
+
static resolve(config: Config, bearer?: string): Promise<OperationCatalog>;
|
|
20
|
+
get isEmpty(): boolean;
|
|
21
|
+
adopt(other: OperationCatalog): void;
|
|
24
22
|
get(opId: string): Operation | undefined;
|
|
25
23
|
operations(): readonly Operation[];
|
|
26
|
-
/**
|
|
27
|
-
* Подсказывает похожие `opId` — нужна для внятной ошибки, когда модель
|
|
28
|
-
* угадала имя операции вместо того, чтобы взять его из поиска.
|
|
29
|
-
*/
|
|
30
24
|
suggest(opId: string, limit?: number): string[];
|
|
31
25
|
search(params: {
|
|
32
26
|
query: string;
|
|
@@ -36,4 +30,3 @@ export declare class OperationCatalog {
|
|
|
36
30
|
limit?: number;
|
|
37
31
|
}): OperationHit[];
|
|
38
32
|
}
|
|
39
|
-
//# sourceMappingURL=catalog.d.ts.map
|
package/dist/api/catalog.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { dataDir } from '../knowledge/loader.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Поиск здесь примитивный — подсчёт совпадений термов по склеенной строке.
|
|
7
|
-
* Это осознанно: путей 478, они короткие и хорошо самоописаны,
|
|
8
|
-
* полнотекстовый движок дал бы ту же выдачу при большем весе.
|
|
9
|
-
*/
|
|
4
|
+
import { buildCatalog } from './build-catalog.js';
|
|
5
|
+
import { fetchSwagger } from './swagger-source.js';
|
|
10
6
|
export class OperationCatalog {
|
|
11
7
|
byOpId;
|
|
12
8
|
catalog;
|
|
@@ -14,13 +10,61 @@ export class OperationCatalog {
|
|
|
14
10
|
if (catalog.version !== 1) {
|
|
15
11
|
throw new Error(`Unsupported catalog version ${String(catalog.version)}`);
|
|
16
12
|
}
|
|
17
|
-
/** Снапшот приходит из файла, поэтому необязательные поля нормализуем на входе. */
|
|
18
13
|
this.catalog = { ...catalog, warnings: catalog.warnings ?? [] };
|
|
19
14
|
this.byOpId = new Map(catalog.operations.map((o) => [o.opId, o]));
|
|
20
15
|
}
|
|
21
|
-
static
|
|
22
|
-
const file = resolve(dataDir(), '
|
|
23
|
-
|
|
16
|
+
static permissionMap() {
|
|
17
|
+
const file = resolve(dataDir(), 'permissions.json');
|
|
18
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
19
|
+
return { byOpId: parsed.byOpId, permissions: parsed.permissions };
|
|
20
|
+
}
|
|
21
|
+
static async resolve(config, bearer) {
|
|
22
|
+
const builtAt = new Date().toISOString();
|
|
23
|
+
try {
|
|
24
|
+
const swagger = await fetchSwagger({
|
|
25
|
+
baseUrl: config.baseUrl,
|
|
26
|
+
cacheDir: config.cacheDir,
|
|
27
|
+
timeoutMs: config.requestTimeoutMs,
|
|
28
|
+
...(bearer ? { bearer } : {}),
|
|
29
|
+
});
|
|
30
|
+
const catalog = buildCatalog({
|
|
31
|
+
rawSwagger: swagger.raw,
|
|
32
|
+
permissions: OperationCatalog.permissionMap(),
|
|
33
|
+
builtAt,
|
|
34
|
+
});
|
|
35
|
+
return new OperationCatalog(swagger.origin === 'cache'
|
|
36
|
+
? {
|
|
37
|
+
...catalog,
|
|
38
|
+
warnings: [
|
|
39
|
+
...catalog.warnings,
|
|
40
|
+
`The instance at ${config.baseUrl} was unreachable; this catalog came from the ` +
|
|
41
|
+
'on-disk cache and may not match the API that is actually running.',
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
: catalog);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return new OperationCatalog({
|
|
48
|
+
version: 1,
|
|
49
|
+
builtAt,
|
|
50
|
+
apiPrefix: '/api/admin',
|
|
51
|
+
swaggerHash: '',
|
|
52
|
+
permissions: [],
|
|
53
|
+
warnings: [
|
|
54
|
+
`The operation catalog is EMPTY: ${config.baseUrl} did not serve its API document ` +
|
|
55
|
+
`(${error instanceof Error ? error.message : String(error)}). ` +
|
|
56
|
+
'No API operation can be searched or called until the instance is reachable.',
|
|
57
|
+
],
|
|
58
|
+
operations: [],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
get isEmpty() {
|
|
63
|
+
return this.catalog.operations.length === 0;
|
|
64
|
+
}
|
|
65
|
+
adopt(other) {
|
|
66
|
+
this.catalog = other.catalog;
|
|
67
|
+
this.byOpId = new Map(other.catalog.operations.map((o) => [o.opId, o]));
|
|
24
68
|
}
|
|
25
69
|
get(opId) {
|
|
26
70
|
return this.byOpId.get(opId);
|
|
@@ -28,10 +72,6 @@ export class OperationCatalog {
|
|
|
28
72
|
operations() {
|
|
29
73
|
return this.catalog.operations;
|
|
30
74
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Подсказывает похожие `opId` — нужна для внятной ошибки, когда модель
|
|
33
|
-
* угадала имя операции вместо того, чтобы взять его из поиска.
|
|
34
|
-
*/
|
|
35
75
|
suggest(opId, limit = 5) {
|
|
36
76
|
const needle = opId.toLowerCase();
|
|
37
77
|
return this.catalog.operations
|
|
@@ -86,7 +126,6 @@ export class OperationCatalog {
|
|
|
86
126
|
return hits.slice(0, params.limit ?? 15);
|
|
87
127
|
}
|
|
88
128
|
}
|
|
89
|
-
/** Доля общих триграмм — грубая, но достаточная мера похожести имён операций. */
|
|
90
129
|
const overlap = (a, b) => {
|
|
91
130
|
const grams = (value) => {
|
|
92
131
|
const out = new Set();
|
|
@@ -105,4 +144,3 @@ const overlap = (a, b) => {
|
|
|
105
144
|
}
|
|
106
145
|
return right.size === 0 ? 0 : shared / right.size;
|
|
107
146
|
};
|
|
108
|
-
//# sourceMappingURL=catalog.js.map
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { TokenStore } from './auth.js';
|
|
2
2
|
import type { Operation } from './types.js';
|
|
3
|
-
/** Аргументы вызова операции, как их присылает модель. */
|
|
4
3
|
export interface CallArgs {
|
|
5
4
|
path?: Record<string, string | number>;
|
|
6
5
|
query?: Record<string, string | number | boolean>;
|
|
7
6
|
body?: unknown;
|
|
8
7
|
}
|
|
9
|
-
/** Нормализованный неуспех вызова. */
|
|
10
8
|
export interface NormalizedError {
|
|
11
9
|
status: number;
|
|
12
10
|
message: string;
|
|
13
11
|
hint?: string;
|
|
14
12
|
}
|
|
15
|
-
/** Результат вызова: либо тело ответа, либо нормализованная ошибка. */
|
|
16
13
|
export type CallResult = {
|
|
17
14
|
ok: true;
|
|
18
15
|
status: number;
|
|
@@ -21,29 +18,10 @@ export type CallResult = {
|
|
|
21
18
|
ok: false;
|
|
22
19
|
error: NormalizedError;
|
|
23
20
|
};
|
|
24
|
-
/** Ошибка на стороне вызывающего — до отправки запроса. */
|
|
25
21
|
export declare class RequestBuildError extends Error {
|
|
26
22
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Возвращает тело в том виде, в каком его надо сериализовать в запрос.
|
|
29
|
-
* Часть MCP-клиентов передаёт объектный аргумент уже сериализованным в строку;
|
|
30
|
-
* без этой нормализации `JSON.stringify` завернул бы его второй раз, и на сервер
|
|
31
|
-
* ушла бы JSON-строка вместо объекта — любая запись падала бы с невнятным 400.
|
|
32
|
-
* Разворачивается только строка, которая целиком является объектом или массивом:
|
|
33
|
-
* скалярная строка — законное тело, и трогать её нельзя.
|
|
34
|
-
*/
|
|
35
23
|
export declare const normalizeBody: (body: unknown) => unknown;
|
|
36
|
-
/**
|
|
37
|
-
* Подставляет path-параметры и собирает query-строку.
|
|
38
|
-
* Отсутствующий обязательный path-параметр — ошибка здесь, а не 404 от сервера:
|
|
39
|
-
* иначе агент получает загадочный ответ вместо указания, чего не хватает.
|
|
40
|
-
*/
|
|
41
24
|
export declare const buildUrl: (baseUrl: string, operation: Operation, args: CallArgs) => string;
|
|
42
|
-
/**
|
|
43
|
-
* HTTP-клиент Admin API.
|
|
44
|
-
* Единственный ретрай — на 401 после обновления токена: любой другой повтор
|
|
45
|
-
* на изменяющей операции рискует создать дубль, поэтому его здесь нет.
|
|
46
|
-
*/
|
|
47
25
|
export declare class AdminApiClient {
|
|
48
26
|
private readonly baseUrl;
|
|
49
27
|
private readonly tokens;
|
|
@@ -54,7 +32,5 @@ export declare class AdminApiClient {
|
|
|
54
32
|
timeoutMs: number;
|
|
55
33
|
});
|
|
56
34
|
call(operation: Operation, args: CallArgs): Promise<CallResult>;
|
|
57
|
-
/** Читает набор прав админа. Пустой массив означает «не удалось выяснить». */
|
|
58
35
|
fetchPermissions(adminId: number): Promise<string[]>;
|
|
59
36
|
}
|
|
60
|
-
//# sourceMappingURL=client.d.ts.map
|
package/dist/api/client.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/** Ошибка на стороне вызывающего — до отправки запроса. */
|
|
2
1
|
export class RequestBuildError extends Error {
|
|
3
2
|
}
|
|
4
|
-
/**
|
|
5
|
-
* Возвращает тело в том виде, в каком его надо сериализовать в запрос.
|
|
6
|
-
* Часть MCP-клиентов передаёт объектный аргумент уже сериализованным в строку;
|
|
7
|
-
* без этой нормализации `JSON.stringify` завернул бы его второй раз, и на сервер
|
|
8
|
-
* ушла бы JSON-строка вместо объекта — любая запись падала бы с невнятным 400.
|
|
9
|
-
* Разворачивается только строка, которая целиком является объектом или массивом:
|
|
10
|
-
* скалярная строка — законное тело, и трогать её нельзя.
|
|
11
|
-
*/
|
|
12
3
|
export const normalizeBody = (body) => {
|
|
13
4
|
if (typeof body !== 'string') {
|
|
14
5
|
return body;
|
|
@@ -25,11 +16,6 @@ export const normalizeBody = (body) => {
|
|
|
25
16
|
return body;
|
|
26
17
|
}
|
|
27
18
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Подставляет path-параметры и собирает query-строку.
|
|
30
|
-
* Отсутствующий обязательный path-параметр — ошибка здесь, а не 404 от сервера:
|
|
31
|
-
* иначе агент получает загадочный ответ вместо указания, чего не хватает.
|
|
32
|
-
*/
|
|
33
19
|
export const buildUrl = (baseUrl, operation, args) => {
|
|
34
20
|
let path = operation.path;
|
|
35
21
|
const provided = args.path ?? {};
|
|
@@ -47,7 +33,7 @@ export const buildUrl = (baseUrl, operation, args) => {
|
|
|
47
33
|
const leftover = /\{([^}]+)\}/.exec(path);
|
|
48
34
|
if (leftover) {
|
|
49
35
|
throw new RequestBuildError(`Path parameter "${leftover[1] ?? ''}" of ${operation.path} was not provided and is not ` +
|
|
50
|
-
'declared in the catalog — the
|
|
36
|
+
'declared in the catalog — the cached API document may be stale.');
|
|
51
37
|
}
|
|
52
38
|
const search = new URLSearchParams();
|
|
53
39
|
for (const [key, value] of Object.entries(args.query ?? {})) {
|
|
@@ -58,23 +44,22 @@ export const buildUrl = (baseUrl, operation, args) => {
|
|
|
58
44
|
const suffix = search.size > 0 ? `?${search.toString()}` : '';
|
|
59
45
|
return `${baseUrl}${path}${suffix}`;
|
|
60
46
|
};
|
|
61
|
-
/** Подбирает подсказку по коду ответа и метаданным операции. */
|
|
62
47
|
const hintFor = (status, operation) => {
|
|
63
48
|
if (status === 400 || status === 422) {
|
|
64
49
|
const loose = operation.body?.schema['x-loose'] === true;
|
|
65
50
|
return (`Validation rejected the payload. ${loose ? 'This body has loosely typed fields — copy the shape from the example in cms_api_describe. ' : ''}` +
|
|
66
|
-
|
|
51
|
+
'Search the knowledge base with cms_docs_search before retrying.');
|
|
67
52
|
}
|
|
68
53
|
if (status === 403) {
|
|
69
54
|
return operation.permission
|
|
70
55
|
? `The admin lacks "${operation.permission}". Ask for the grant; retrying will not help.`
|
|
71
|
-
: 'Forbidden. Check the admin\'s permissions and the module visibility rules
|
|
56
|
+
: 'Forbidden. Check the admin\'s permissions and the module visibility rules.';
|
|
72
57
|
}
|
|
73
58
|
if (status === 404) {
|
|
74
|
-
return 'Not found. The id may belong to another
|
|
59
|
+
return 'Not found. The id may belong to another instance — prefer marker-based operations where they exist.';
|
|
75
60
|
}
|
|
76
61
|
if (status >= 500) {
|
|
77
|
-
return 'Server-side failure
|
|
62
|
+
return 'Server-side failure on the instance. Report it to your operator; do not retry blindly.';
|
|
78
63
|
}
|
|
79
64
|
return undefined;
|
|
80
65
|
};
|
|
@@ -105,11 +90,6 @@ const extractMessage = (body, fallback) => {
|
|
|
105
90
|
}
|
|
106
91
|
return fallback;
|
|
107
92
|
};
|
|
108
|
-
/**
|
|
109
|
-
* HTTP-клиент Admin API.
|
|
110
|
-
* Единственный ретрай — на 401 после обновления токена: любой другой повтор
|
|
111
|
-
* на изменяющей операции рискует создать дубль, поэтому его здесь нет.
|
|
112
|
-
*/
|
|
113
93
|
export class AdminApiClient {
|
|
114
94
|
baseUrl;
|
|
115
95
|
tokens;
|
|
@@ -148,7 +128,7 @@ export class AdminApiClient {
|
|
|
148
128
|
error: {
|
|
149
129
|
status: 0,
|
|
150
130
|
message: `Request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
-
hint: 'Is the
|
|
131
|
+
hint: 'Is the instance reachable at the configured base URL? It must expose the Admin API under /api/admin.',
|
|
152
132
|
},
|
|
153
133
|
};
|
|
154
134
|
}
|
|
@@ -166,7 +146,6 @@ export class AdminApiClient {
|
|
|
166
146
|
}
|
|
167
147
|
return { ok: true, status: response.status, body };
|
|
168
148
|
}
|
|
169
|
-
/** Читает набор прав админа. Пустой массив означает «не удалось выяснить». */
|
|
170
149
|
async fetchPermissions(adminId) {
|
|
171
150
|
try {
|
|
172
151
|
const token = await this.tokens.accessToken();
|
|
@@ -193,4 +172,3 @@ export class AdminApiClient {
|
|
|
193
172
|
}
|
|
194
173
|
}
|
|
195
174
|
}
|
|
196
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const VALID_TYPES = new Set(['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']);
|
|
2
|
+
const TYPE_MAP = {
|
|
3
|
+
date: { type: 'string', format: 'date-time' },
|
|
4
|
+
Date: { type: 'string', format: 'date-time' },
|
|
5
|
+
'iso-date': { type: 'string', format: 'date-time' },
|
|
6
|
+
float: { type: 'number' },
|
|
7
|
+
int: { type: 'integer' },
|
|
8
|
+
json: { type: 'object' },
|
|
9
|
+
list: { type: 'array' },
|
|
10
|
+
text: { type: 'string' },
|
|
11
|
+
textWithHeader: { type: 'string' },
|
|
12
|
+
image: { type: 'object' },
|
|
13
|
+
file: { type: 'object' },
|
|
14
|
+
'number[]': { type: 'array', items: { type: 'number' } },
|
|
15
|
+
'string[]': { type: 'array', items: { type: 'string' } },
|
|
16
|
+
};
|
|
17
|
+
const MAX_DESCRIPTION = 240;
|
|
18
|
+
const MAX_DEPTH = 6;
|
|
19
|
+
const trimDescription = (value) => {
|
|
20
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
24
|
+
return flat.length > MAX_DESCRIPTION ? `${flat.slice(0, MAX_DESCRIPTION)}…` : flat;
|
|
25
|
+
};
|
|
26
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
export const normalizeSchema = (node, components, depth = 0, refStack = []) => {
|
|
28
|
+
if (!isRecord(node)) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const ref = node['$ref'];
|
|
32
|
+
if (typeof ref === 'string') {
|
|
33
|
+
const name = ref.split('/').pop() ?? '';
|
|
34
|
+
if (refStack.includes(name)) {
|
|
35
|
+
return { type: 'object', description: `(recursive ${name}, omitted)` };
|
|
36
|
+
}
|
|
37
|
+
const target = components[name];
|
|
38
|
+
if (target === undefined) {
|
|
39
|
+
return { type: 'object', description: `(unresolved ${name})` };
|
|
40
|
+
}
|
|
41
|
+
return normalizeSchema(target, components, depth, [...refStack, name]);
|
|
42
|
+
}
|
|
43
|
+
for (const key of ['allOf', 'oneOf', 'anyOf']) {
|
|
44
|
+
const variants = node[key];
|
|
45
|
+
if (Array.isArray(variants) && variants.length > 0) {
|
|
46
|
+
const merged = normalizeSchema(variants[0], components, depth, refStack);
|
|
47
|
+
const description = trimDescription(node['description']);
|
|
48
|
+
return description ? { ...merged, description } : merged;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const out = {};
|
|
52
|
+
const rawType = node['type'];
|
|
53
|
+
if (typeof rawType === 'string') {
|
|
54
|
+
if (VALID_TYPES.has(rawType)) {
|
|
55
|
+
out.type = rawType;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const mapped = TYPE_MAP[rawType];
|
|
59
|
+
if (mapped) {
|
|
60
|
+
out.type = mapped.type;
|
|
61
|
+
if (mapped.format) {
|
|
62
|
+
out.format = mapped.format;
|
|
63
|
+
}
|
|
64
|
+
if (mapped.items) {
|
|
65
|
+
out.items = mapped.items;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (rawType.startsWith('Record<')) {
|
|
69
|
+
out.type = 'object';
|
|
70
|
+
}
|
|
71
|
+
out['x-source-type'] = rawType;
|
|
72
|
+
out['x-loose'] = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const format = node['format'];
|
|
76
|
+
if (typeof format === 'string' && !out.format) {
|
|
77
|
+
out.format = format;
|
|
78
|
+
}
|
|
79
|
+
const description = trimDescription(node['description']);
|
|
80
|
+
if (description) {
|
|
81
|
+
out.description = description;
|
|
82
|
+
}
|
|
83
|
+
if (node['example'] !== undefined) {
|
|
84
|
+
out.example = node['example'];
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(node['enum'])) {
|
|
87
|
+
out.enum = node['enum'];
|
|
88
|
+
}
|
|
89
|
+
if (depth >= MAX_DEPTH) {
|
|
90
|
+
out['x-truncated'] = true;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
if (node['items'] !== undefined) {
|
|
94
|
+
out.items = normalizeSchema(node['items'], components, depth + 1, refStack);
|
|
95
|
+
if (!out.type) {
|
|
96
|
+
out.type = 'array';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const properties = node['properties'];
|
|
100
|
+
if (isRecord(properties)) {
|
|
101
|
+
out.type = out.type ?? 'object';
|
|
102
|
+
out.properties = {};
|
|
103
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
104
|
+
out.properties[key] = normalizeSchema(value, components, depth + 1, refStack);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const required = node['required'];
|
|
108
|
+
if (Array.isArray(required) && required.every((r) => typeof r === 'string')) {
|
|
109
|
+
out.required = required;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
const MAX_SCHEMA_BYTES = 6_144;
|
|
114
|
+
export const capSchema = (schema) => {
|
|
115
|
+
if (Buffer.byteLength(JSON.stringify(schema), 'utf8') <= MAX_SCHEMA_BYTES) {
|
|
116
|
+
return schema;
|
|
117
|
+
}
|
|
118
|
+
if (!schema.properties) {
|
|
119
|
+
return { ...schema, 'x-truncated': true };
|
|
120
|
+
}
|
|
121
|
+
const shallow = {};
|
|
122
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
123
|
+
shallow[key] = {
|
|
124
|
+
...(value.type ? { type: value.type } : {}),
|
|
125
|
+
...(value.format ? { format: value.format } : {}),
|
|
126
|
+
...(value.description ? { description: value.description } : {}),
|
|
127
|
+
...(value.example !== undefined ? { example: value.example } : {}),
|
|
128
|
+
...(value['x-loose'] ? { 'x-loose': true } : {}),
|
|
129
|
+
...(value.properties || value.items ? { 'x-truncated': true } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { ...schema, properties: shallow, 'x-truncated': true };
|
|
133
|
+
};
|
package/dist/api/policy.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AllowLevel } from '../config/config.js';
|
|
2
2
|
import type { AdminIdentity } from './auth.js';
|
|
3
3
|
import type { Operation } from './types.js';
|
|
4
|
-
/** Результат проверки политики перед выполнением операции. */
|
|
5
4
|
export type PolicyDecision = {
|
|
6
5
|
kind: 'allow';
|
|
7
6
|
} | {
|
|
@@ -11,42 +10,20 @@ export type PolicyDecision = {
|
|
|
11
10
|
kind: 'needsConfirm';
|
|
12
11
|
reason: string;
|
|
13
12
|
};
|
|
14
|
-
/**
|
|
15
|
-
* Одноразовые токены подтверждения для необратимых операций.
|
|
16
|
-
* Токен привязан к хэшу (opId + аргументы), поэтому подтверждение одного удаления
|
|
17
|
-
* нельзя переиспользовать для другого — иначе двухшаговость была бы декоративной.
|
|
18
|
-
*/
|
|
19
13
|
export declare class ConfirmStore {
|
|
20
14
|
private readonly issued;
|
|
21
15
|
private static hash;
|
|
22
16
|
issue(opId: string, args: unknown, now?: number): string;
|
|
23
|
-
/**
|
|
24
|
-
* Проверяет токен, не гася его.
|
|
25
|
-
* Нужна, чтобы отказ по другой причине (нет права, не тот уровень allow)
|
|
26
|
-
* не сжигал подтверждение, которое человек уже дал.
|
|
27
|
-
*/
|
|
28
17
|
verify(token: string, opId: string, args: unknown, now?: number): boolean;
|
|
29
|
-
/** Проверяет и немедленно гасит токен. Повторное использование невозможно. */
|
|
30
18
|
consume(token: string, opId: string, args: unknown, now?: number): boolean;
|
|
31
19
|
private sweep;
|
|
32
20
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Проверяет только уровень `--allow`.
|
|
35
|
-
* Вынесено отдельно, чтобы вызывающий мог отказать до аутентификации:
|
|
36
|
-
* обещание «no request was sent» не должно нарушаться самим логином.
|
|
37
|
-
*/
|
|
38
21
|
export declare const checkLevel: (operation: Operation, allow: AllowLevel) => Extract<PolicyDecision, {
|
|
39
22
|
kind: "deny";
|
|
40
23
|
}> | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* Решает, можно ли выполнить операцию.
|
|
43
|
-
* Порядок проверок значим: уровень доступа — до прав, права — до подтверждения,
|
|
44
|
-
* чтобы read-only сервер вообще не выдавал токенов подтверждения.
|
|
45
|
-
*/
|
|
46
24
|
export declare const decide: (params: {
|
|
47
25
|
operation: Operation;
|
|
48
26
|
allow: AllowLevel;
|
|
49
27
|
identity?: AdminIdentity;
|
|
50
28
|
confirmValid: boolean;
|
|
51
29
|
}) => PolicyDecision;
|
|
52
|
-
//# sourceMappingURL=policy.d.ts.map
|
package/dist/api/policy.js
CHANGED
|
Binary file
|