@pptb/types 1.2.3 → 1.2.4-beta.1
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/bin/pptb-validate.js +1 -243
- package/package.json +4 -1
- package/lib/validate.js +0 -486
package/bin/pptb-validate.js
CHANGED
|
@@ -1,244 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
"use strict";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* pptb-validate - CLI tool for validating Power Platform ToolBox tool packages
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* pptb-validate [options] [path/to/package.json]
|
|
10
|
-
*
|
|
11
|
-
* Options:
|
|
12
|
-
* --skip-url-checks Skip URL accessibility checks (faster, offline-friendly)
|
|
13
|
-
* --json Output results as JSON
|
|
14
|
-
* --help, -h Show help information
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const fs = require("fs");
|
|
18
|
-
const path = require("path");
|
|
19
|
-
const { validatePackageJson, validatePPTBConfig, KNOWN_CAPABILITY_TAGS } = require("../lib/validate");
|
|
20
|
-
|
|
21
|
-
// ANSI colour helpers – gracefully degrade when colours are unsupported
|
|
22
|
-
const NO_COLOR = !process.stdout.isTTY || process.env.NO_COLOR;
|
|
23
|
-
const c = {
|
|
24
|
-
red: (s) => (NO_COLOR ? s : `\x1b[31m${s}\x1b[0m`),
|
|
25
|
-
yellow: (s) => (NO_COLOR ? s : `\x1b[33m${s}\x1b[0m`),
|
|
26
|
-
green: (s) => (NO_COLOR ? s : `\x1b[32m${s}\x1b[0m`),
|
|
27
|
-
cyan: (s) => (NO_COLOR ? s : `\x1b[36m${s}\x1b[0m`),
|
|
28
|
-
bold: (s) => (NO_COLOR ? s : `\x1b[1m${s}\x1b[0m`),
|
|
29
|
-
dim: (s) => (NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function printHelp() {
|
|
33
|
-
console.log(`
|
|
34
|
-
${c.bold("pptb-validate")} – Power Platform ToolBox tool validator
|
|
35
|
-
|
|
36
|
-
${c.bold("USAGE")}
|
|
37
|
-
pptb-validate [options] [path/to/package.json]
|
|
38
|
-
|
|
39
|
-
When no path is given the tool looks for ${c.cyan("package.json")} in the current
|
|
40
|
-
working directory. If a ${c.cyan("pptb.config.json")} file exists in the same directory
|
|
41
|
-
it is automatically validated as well.
|
|
42
|
-
|
|
43
|
-
${c.bold("OPTIONS")}
|
|
44
|
-
${c.cyan("--skip-url-checks")} Skip URL reachability checks (faster, works offline)
|
|
45
|
-
${c.cyan("--json")} Print results as a JSON object instead of human-readable text
|
|
46
|
-
${c.cyan("--help")}, ${c.cyan("-h")} Show this help message
|
|
47
|
-
|
|
48
|
-
${c.bold("ADD TO YOUR TOOL'S package.json")}
|
|
49
|
-
${c.dim(`"scripts": {
|
|
50
|
-
"validate": "pptb-validate"
|
|
51
|
-
}`)}
|
|
52
|
-
|
|
53
|
-
Then run: ${c.cyan("npm run validate")}
|
|
54
|
-
|
|
55
|
-
${c.bold("EXAMPLES")}
|
|
56
|
-
npm run validate
|
|
57
|
-
npm run validate ./my-tool/package.json
|
|
58
|
-
npm run validate --skip-url-checks
|
|
59
|
-
npm run validate --json
|
|
60
|
-
`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
const args = process.argv.slice(2);
|
|
65
|
-
|
|
66
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
67
|
-
printHelp();
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const skipUrlChecks = args.includes("--skip-url-checks");
|
|
72
|
-
const jsonOutput = args.includes("--json");
|
|
73
|
-
|
|
74
|
-
// Find the package.json path from positional args (skip flags)
|
|
75
|
-
const positional = args.filter((a) => !a.startsWith("-"));
|
|
76
|
-
let packageJsonPath = positional[0] || path.join(process.cwd(), "package.json");
|
|
77
|
-
|
|
78
|
-
// Resolve to absolute path
|
|
79
|
-
if (!path.isAbsolute(packageJsonPath)) {
|
|
80
|
-
packageJsonPath = path.resolve(process.cwd(), packageJsonPath);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Derive pptb.config.json path from the same directory as package.json
|
|
84
|
-
const toolDir = path.dirname(packageJsonPath);
|
|
85
|
-
const pptbConfigPath = path.join(toolDir, "pptb.config.json");
|
|
86
|
-
|
|
87
|
-
// --- Load package.json ---
|
|
88
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
89
|
-
if (jsonOutput) {
|
|
90
|
-
console.log(JSON.stringify({ valid: false, errors: [`package.json not found at: ${packageJsonPath}`], warnings: [] }, null, 2));
|
|
91
|
-
} else {
|
|
92
|
-
console.error(c.red(`✖ package.json not found at: ${packageJsonPath}`));
|
|
93
|
-
}
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let packageJson;
|
|
98
|
-
try {
|
|
99
|
-
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
100
|
-
} catch (err) {
|
|
101
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
-
if (jsonOutput) {
|
|
103
|
-
console.log(JSON.stringify({ valid: false, errors: [`Failed to parse package.json: ${message}`], warnings: [] }, null, 2));
|
|
104
|
-
} else {
|
|
105
|
-
console.error(c.red(`✖ Failed to parse package.json: ${message}`));
|
|
106
|
-
}
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// --- Load pptb.config.json (optional) ---
|
|
111
|
-
let pptbConfig = null;
|
|
112
|
-
let pptbConfigParseError = null;
|
|
113
|
-
if (fs.existsSync(pptbConfigPath)) {
|
|
114
|
-
try {
|
|
115
|
-
pptbConfig = JSON.parse(fs.readFileSync(pptbConfigPath, "utf8"));
|
|
116
|
-
} catch (err) {
|
|
117
|
-
pptbConfigParseError = err instanceof Error ? err.message : String(err);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// --- Run validation ---
|
|
122
|
-
if (!jsonOutput) {
|
|
123
|
-
console.log();
|
|
124
|
-
console.log(c.bold("Power Platform ToolBox – Tool Validator"));
|
|
125
|
-
console.log(c.dim("─".repeat(45)));
|
|
126
|
-
console.log(c.dim(`File: ${packageJsonPath}`));
|
|
127
|
-
if (pptbConfig !== null) {
|
|
128
|
-
console.log(c.dim(`Config: ${pptbConfigPath}`));
|
|
129
|
-
}
|
|
130
|
-
if (skipUrlChecks) {
|
|
131
|
-
console.log(c.yellow("⚠ URL reachability checks are skipped"));
|
|
132
|
-
}
|
|
133
|
-
console.log();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let packageResult;
|
|
137
|
-
try {
|
|
138
|
-
packageResult = await validatePackageJson(packageJson, { skipUrlChecks });
|
|
139
|
-
} catch (err) {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
if (jsonOutput) {
|
|
142
|
-
console.log(JSON.stringify({ valid: false, errors: [`Unexpected validation error: ${message}`], warnings: [] }, null, 2));
|
|
143
|
-
} else {
|
|
144
|
-
console.error(c.red(`✖ Unexpected validation error: ${message}`));
|
|
145
|
-
}
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// --- Validate pptb.config.json if present ---
|
|
150
|
-
let configResult = null;
|
|
151
|
-
if (pptbConfigParseError !== null) {
|
|
152
|
-
configResult = { valid: false, errors: [`Failed to parse pptb.config.json: ${pptbConfigParseError}`], warnings: [] };
|
|
153
|
-
} else if (pptbConfig !== null) {
|
|
154
|
-
configResult = validatePPTBConfig(pptbConfig);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Merge results for overall pass/fail
|
|
158
|
-
const allErrors = [...packageResult.errors, ...(configResult ? configResult.errors : [])];
|
|
159
|
-
const allWarnings = [...packageResult.warnings, ...(configResult ? configResult.warnings : [])];
|
|
160
|
-
const overallValid = packageResult.valid && (configResult === null || configResult.valid);
|
|
161
|
-
|
|
162
|
-
// --- Output results ---
|
|
163
|
-
if (jsonOutput) {
|
|
164
|
-
const output = {
|
|
165
|
-
valid: overallValid,
|
|
166
|
-
errors: allErrors,
|
|
167
|
-
warnings: allWarnings,
|
|
168
|
-
packageInfo: packageResult.packageInfo,
|
|
169
|
-
configInfo: configResult ? configResult.packageInfo : undefined,
|
|
170
|
-
};
|
|
171
|
-
console.log(JSON.stringify(output, null, 2));
|
|
172
|
-
process.exit(overallValid ? 0 : 1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Human-readable output – package.json section
|
|
176
|
-
if (packageResult.errors.length > 0) {
|
|
177
|
-
console.log(c.bold(c.red(`package.json Errors (${packageResult.errors.length})`)));
|
|
178
|
-
packageResult.errors.forEach((e) => console.log(` ${c.red("✖")} ${e}`));
|
|
179
|
-
console.log();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (packageResult.warnings.length > 0) {
|
|
183
|
-
console.log(c.bold(c.yellow(`package.json Warnings (${packageResult.warnings.length})`)));
|
|
184
|
-
packageResult.warnings.forEach((w) => console.log(` ${c.yellow("⚠")} ${w}`));
|
|
185
|
-
console.log();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Human-readable output – pptb.config.json section
|
|
189
|
-
if (configResult !== null) {
|
|
190
|
-
if (configResult.errors.length > 0) {
|
|
191
|
-
console.log(c.bold(c.red(`pptb.config.json Errors (${configResult.errors.length})`)));
|
|
192
|
-
configResult.errors.forEach((e) => console.log(` ${c.red("✖")} ${e}`));
|
|
193
|
-
console.log();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (configResult.warnings.length > 0) {
|
|
197
|
-
console.log(c.bold(c.yellow(`pptb.config.json Warnings (${configResult.warnings.length})`)));
|
|
198
|
-
configResult.warnings.forEach((w) => console.log(` ${c.yellow("⚠")} ${w}`));
|
|
199
|
-
console.log();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (overallValid) {
|
|
204
|
-
const info = packageResult.packageInfo;
|
|
205
|
-
console.log(c.green(c.bold("✔ Validation passed")));
|
|
206
|
-
console.log();
|
|
207
|
-
console.log(c.bold("Package summary"));
|
|
208
|
-
console.log(c.dim("─".repeat(45)));
|
|
209
|
-
console.log(` Name : ${info.name}`);
|
|
210
|
-
console.log(` Version : ${info.version}`);
|
|
211
|
-
console.log(` Display name: ${info.displayName}`);
|
|
212
|
-
console.log(` Description : ${info.description}`);
|
|
213
|
-
console.log(` License : ${info.license}`);
|
|
214
|
-
console.log(` Contributors: ${info.contributors.map((contributor) => contributor.name).join(", ")}`);
|
|
215
|
-
if (info.icon) {
|
|
216
|
-
console.log(` Icon : ${info.icon}`);
|
|
217
|
-
}
|
|
218
|
-
if (info.features) {
|
|
219
|
-
console.log(` Features : multiConnection=${info.features.multiConnection}${info.features.minAPI ? `, minAPI=${info.features.minAPI}` : ""}`);
|
|
220
|
-
}
|
|
221
|
-
if (configResult !== null && configResult.packageInfo && configResult.packageInfo.invocation) {
|
|
222
|
-
const inv = configResult.packageInfo.invocation;
|
|
223
|
-
console.log(` Invocation : version=${inv.version}`);
|
|
224
|
-
if (Array.isArray(inv.capabilities) && inv.capabilities.length > 0) {
|
|
225
|
-
console.log(` Capabilities: ${inv.capabilities.join(", ")}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
console.log();
|
|
229
|
-
console.log(c.dim(`Known capability tags: ${KNOWN_CAPABILITY_TAGS.join(", ")}`));
|
|
230
|
-
console.log();
|
|
231
|
-
} else {
|
|
232
|
-
console.log(c.red(c.bold("✖ Validation failed")));
|
|
233
|
-
console.log();
|
|
234
|
-
console.log(c.dim("Fix the errors listed above and re-run pptb-validate before publishing."));
|
|
235
|
-
console.log();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
process.exit(overallValid ? 0 : 1);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
main().catch((err) => {
|
|
242
|
-
console.error(c.red(`✖ Fatal error: ${err instanceof Error ? err.message : String(err)}`));
|
|
243
|
-
process.exit(1);
|
|
244
|
-
});
|
|
2
|
+
require("@pptb/validate/cli");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pptb/types",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4-beta.1",
|
|
4
4
|
"description": "Type definitions for Power Platform ToolBox APIs and validity checks for tool packages",
|
|
5
5
|
"main": "index.d.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"pptb-validate": "./bin/pptb-validate.js"
|
|
24
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@pptb/validate": "^0.0.1"
|
|
27
|
+
},
|
|
25
28
|
"files": [
|
|
26
29
|
"index.d.ts",
|
|
27
30
|
"toolboxAPI.d.ts",
|
package/lib/validate.js
DELETED
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Tool validation logic for Power Platform ToolBox tools.
|
|
6
|
-
* Mirrors the validation rules used during the official review process.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** @typedef {{ name: string; url?: string }} Contributor */
|
|
10
|
-
/** @typedef {{ "connect-src"?: string[]; "script-src"?: string[]; "style-src"?: string[]; "img-src"?: string[]; "font-src"?: string[]; "frame-src"?: string[]; "media-src"?: string[] }} CspExceptions */
|
|
11
|
-
/** @typedef {{ repository?: string; website?: string; funding?: string; readmeUrl?: string }} Configurations */
|
|
12
|
-
/** @typedef {{ multiConnection?: "required" | "optional" | "none"; minAPI?: string }} Features */
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {{
|
|
15
|
-
* name: string;
|
|
16
|
-
* version: string;
|
|
17
|
-
* displayName?: string;
|
|
18
|
-
* description?: string;
|
|
19
|
-
* contributors?: Contributor[];
|
|
20
|
-
* cspExceptions?: CspExceptions;
|
|
21
|
-
* license?: string;
|
|
22
|
-
* icon?: string;
|
|
23
|
-
* configurations?: Configurations;
|
|
24
|
-
* features?: Features;
|
|
25
|
-
* }} ToolPackageJson
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* @typedef {{
|
|
29
|
-
* valid: boolean;
|
|
30
|
-
* errors: string[];
|
|
31
|
-
* warnings: string[];
|
|
32
|
-
* packageInfo?: object;
|
|
33
|
-
* }} ValidationResult
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {{ type?: string; enum?: string[]; items?: object }} JsonSchemaProperty
|
|
38
|
-
* @typedef {{ properties?: Record<string, JsonSchemaProperty> }} JsonSchemaObject
|
|
39
|
-
* @typedef {{
|
|
40
|
-
* version: string;
|
|
41
|
-
* prefill?: JsonSchemaObject;
|
|
42
|
-
* returnTopic?: JsonSchemaObject;
|
|
43
|
-
* }} InvocationConfig
|
|
44
|
-
* @typedef {{
|
|
45
|
-
* invocation?: InvocationConfig;
|
|
46
|
-
* }} PPTBConfig
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
// List of approved open source licenses
|
|
50
|
-
const APPROVED_LICENSES = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "GPL-2.0", "GPL-3.0", "LGPL-3.0", "ISC", "AGPL-3.0-only"];
|
|
51
|
-
|
|
52
|
-
// Valid multiConnection values
|
|
53
|
-
const VALID_MULTI_CONNECTION_VALUES = ["required", "optional", "none"];
|
|
54
|
-
|
|
55
|
-
// Semver regex for minAPI validation
|
|
56
|
-
const SEMVER_REGEX = /^\d+\.\d+\.\d+(-[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Built-in list of well-known capability tags.
|
|
60
|
-
* Mirrors the entries in the Supabase `capability_tags` table.
|
|
61
|
-
* New tags are added to Supabase first (configurable without a deploy) and are
|
|
62
|
-
* then reflected here in the next package release so offline validation stays current.
|
|
63
|
-
*
|
|
64
|
-
* When a tool declares a tag that is not in this list the validator emits a warning
|
|
65
|
-
* (not an error) so existing tools are not broken by registry updates.
|
|
66
|
-
*
|
|
67
|
-
* **Keep in sync with `BUILT_IN_CAPABILITY_TAGS` in
|
|
68
|
-
* `src/main/managers/toolRegistryManager.ts`.** Because this file is a standalone
|
|
69
|
-
* Node.js module that cannot import from the Electron/TypeScript source tree, both
|
|
70
|
-
* lists must be updated together whenever a new tag is added.
|
|
71
|
-
*/
|
|
72
|
-
const KNOWN_CAPABILITY_TAGS = [
|
|
73
|
-
"fetchxml",
|
|
74
|
-
"entity-picker",
|
|
75
|
-
"record-selector",
|
|
76
|
-
"solution-selector",
|
|
77
|
-
"webresource-editor",
|
|
78
|
-
"plugin-inspector",
|
|
79
|
-
"pcf-control-builder",
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Checks if a string is a valid URL.
|
|
84
|
-
* @param {string} url
|
|
85
|
-
* @returns {boolean}
|
|
86
|
-
*/
|
|
87
|
-
function isValidUrl(url) {
|
|
88
|
-
try {
|
|
89
|
-
new URL(url);
|
|
90
|
-
return true;
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Checks if a URL hostname is a GitHub domain.
|
|
98
|
-
* @param {string} url
|
|
99
|
-
* @returns {boolean}
|
|
100
|
-
*/
|
|
101
|
-
function isGithubDomain(url) {
|
|
102
|
-
try {
|
|
103
|
-
const hostname = new URL(url).hostname.toLowerCase();
|
|
104
|
-
return hostname === "github.com" || hostname.endsWith(".github.com");
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Validates an icon path string.
|
|
112
|
-
* @param {string} fieldName
|
|
113
|
-
* @param {string} iconPath
|
|
114
|
-
* @param {string[]} errors
|
|
115
|
-
*/
|
|
116
|
-
function validateIconPath(fieldName, iconPath, errors) {
|
|
117
|
-
if (iconPath.startsWith("http://") || iconPath.startsWith("https://")) {
|
|
118
|
-
errors.push(`${fieldName} cannot be an HTTP(S) URL - icons must be bundled under dist`);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (iconPath.startsWith("/")) {
|
|
122
|
-
errors.push(`${fieldName} must be a relative path (e.g., 'icon.svg' or 'icons/icon.svg')`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
// Reject Windows absolute paths like "C:\..." or "C:/"
|
|
126
|
-
if (/^[a-zA-Z]:/.test(iconPath)) {
|
|
127
|
-
errors.push(`${fieldName} must be a relative path (e.g., 'icon.svg' or 'icons/icon.svg')`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
// Reject backslashes — icon paths must use forward slashes
|
|
131
|
-
if (iconPath.includes("\\")) {
|
|
132
|
-
errors.push(`${fieldName} must use forward slashes, not backslashes (e.g., 'icons/icon.svg')`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (iconPath.includes("..")) {
|
|
136
|
-
errors.push(`${fieldName} cannot contain '..' (path traversal not allowed)`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (!iconPath.toLowerCase().endsWith(".svg")) {
|
|
140
|
-
errors.push(`${fieldName} must be an SVG file with .svg extension`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Checks if a URL is accessible by making a HEAD request.
|
|
146
|
-
* @param {string} url
|
|
147
|
-
* @returns {Promise<boolean>}
|
|
148
|
-
*/
|
|
149
|
-
async function isUrlAccessible(url) {
|
|
150
|
-
try {
|
|
151
|
-
const response = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
152
|
-
return response.ok;
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Validates a tool's package.json against the official review criteria.
|
|
160
|
-
*
|
|
161
|
-
* @param {ToolPackageJson} packageJson - The parsed package.json object.
|
|
162
|
-
* @param {{ skipUrlChecks?: boolean }} [options] - Validation options.
|
|
163
|
-
* @returns {Promise<ValidationResult>}
|
|
164
|
-
*/
|
|
165
|
-
async function validatePackageJson(packageJson, options = {}) {
|
|
166
|
-
const { skipUrlChecks = false } = options;
|
|
167
|
-
const errors = /** @type {string[]} */ ([]);
|
|
168
|
-
const warnings = /** @type {string[]} */ ([]);
|
|
169
|
-
|
|
170
|
-
// Required fields
|
|
171
|
-
if (!packageJson.name || typeof packageJson.name !== "string") {
|
|
172
|
-
errors.push("Package name is required and must be a string");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!packageJson.version || typeof packageJson.version !== "string") {
|
|
176
|
-
errors.push("Package version is required and must be a string");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!packageJson.displayName || typeof packageJson.displayName !== "string") {
|
|
180
|
-
errors.push("displayName is required and must be a string");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!packageJson.description || typeof packageJson.description !== "string") {
|
|
184
|
-
errors.push("description is required and must be a string");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// License validation
|
|
188
|
-
if (!packageJson.license) {
|
|
189
|
-
errors.push("license is required");
|
|
190
|
-
} else if (!APPROVED_LICENSES.includes(packageJson.license)) {
|
|
191
|
-
errors.push(`License "${packageJson.license}" is not in the approved list. Approved licenses: ${APPROVED_LICENSES.join(", ")}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Icon validation (optional, but must be a relative SVG path if provided)
|
|
195
|
-
if (packageJson.icon === undefined || packageJson.icon === null) {
|
|
196
|
-
warnings.push("icon is not set; consider adding a bundled SVG icon so your tool displays properly in the marketplace");
|
|
197
|
-
} else if (typeof packageJson.icon !== "string") {
|
|
198
|
-
errors.push("icon must be a string (relative path to bundled SVG under dist)");
|
|
199
|
-
} else {
|
|
200
|
-
validateIconPath("icon", packageJson.icon, errors);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Contributors validation
|
|
204
|
-
if (!packageJson.contributors || !Array.isArray(packageJson.contributors)) {
|
|
205
|
-
errors.push("contributors is required and must be an array");
|
|
206
|
-
} else if (packageJson.contributors.length === 0) {
|
|
207
|
-
errors.push("At least one contributor is required");
|
|
208
|
-
} else {
|
|
209
|
-
packageJson.contributors.forEach((contributor, index) => {
|
|
210
|
-
if (!contributor.name || typeof contributor.name !== "string") {
|
|
211
|
-
errors.push(`Contributor at index ${index} must have a name`);
|
|
212
|
-
}
|
|
213
|
-
if (contributor.url && !isValidUrl(contributor.url)) {
|
|
214
|
-
warnings.push(`Contributor "${contributor.name}" has an invalid URL`);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Configurations validation
|
|
220
|
-
if (!packageJson.configurations || typeof packageJson.configurations !== "object") {
|
|
221
|
-
errors.push("configurations is required and must include repository and readmeUrl");
|
|
222
|
-
} else {
|
|
223
|
-
const configs = packageJson.configurations;
|
|
224
|
-
|
|
225
|
-
// configurations.iconUrl is no longer supported
|
|
226
|
-
if (/** @type {Record<string, unknown>} */ (configs).iconUrl !== undefined) {
|
|
227
|
-
errors.push("configurations.iconUrl is no longer supported; use top-level 'icon' for bundled SVG path");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Repository validation
|
|
231
|
-
if (!configs.repository || typeof configs.repository !== "string") {
|
|
232
|
-
errors.push("configurations.repository is required and must be a URL");
|
|
233
|
-
} else if (!isValidUrl(configs.repository)) {
|
|
234
|
-
errors.push("configurations.repository has an invalid URL format");
|
|
235
|
-
} else if (!skipUrlChecks) {
|
|
236
|
-
const accessible = await isUrlAccessible(configs.repository);
|
|
237
|
-
if (!accessible) {
|
|
238
|
-
errors.push("configurations.repository URL is not accessible");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Website validation (optional but recommended)
|
|
243
|
-
if (!configs.website) {
|
|
244
|
-
warnings.push("configurations.website is not set; consider adding a URL where users can learn more about your tool");
|
|
245
|
-
} else if (!isValidUrl(configs.website)) {
|
|
246
|
-
warnings.push("configurations.website has an invalid URL format");
|
|
247
|
-
} else if (!skipUrlChecks) {
|
|
248
|
-
const accessible = await isUrlAccessible(configs.website);
|
|
249
|
-
if (!accessible) {
|
|
250
|
-
warnings.push("configurations.website URL is not accessible");
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Funding validation (optional but recommended)
|
|
255
|
-
if (configs.funding) {
|
|
256
|
-
if (!isValidUrl(configs.funding)) {
|
|
257
|
-
warnings.push("configurations.funding has an invalid URL format");
|
|
258
|
-
} else if (!skipUrlChecks) {
|
|
259
|
-
const accessible = await isUrlAccessible(configs.funding);
|
|
260
|
-
if (!accessible) {
|
|
261
|
-
warnings.push("configurations.funding URL is not accessible");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ReadmeUrl validation
|
|
267
|
-
if (!configs.readmeUrl || typeof configs.readmeUrl !== "string") {
|
|
268
|
-
errors.push("configurations.readmeUrl is required and must be a URL");
|
|
269
|
-
} else if (!isValidUrl(configs.readmeUrl)) {
|
|
270
|
-
errors.push("configurations.readmeUrl has an invalid URL format");
|
|
271
|
-
} else if (isGithubDomain(configs.readmeUrl)) {
|
|
272
|
-
errors.push("configurations.readmeUrl cannot be hosted on github.com; use raw.githubusercontent.com or another domain");
|
|
273
|
-
} else if (!skipUrlChecks) {
|
|
274
|
-
const accessible = await isUrlAccessible(configs.readmeUrl);
|
|
275
|
-
if (!accessible) {
|
|
276
|
-
errors.push("configurations.readmeUrl is not accessible");
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// CSP Exceptions validation (optional, but validated if present)
|
|
282
|
-
if (packageJson.cspExceptions) {
|
|
283
|
-
const cspExceptions = packageJson.cspExceptions;
|
|
284
|
-
|
|
285
|
-
if (typeof cspExceptions !== "object" || cspExceptions === null || Array.isArray(cspExceptions)) {
|
|
286
|
-
errors.push("cspExceptions must be an object mapping CSP directives to arrays of strings");
|
|
287
|
-
} else {
|
|
288
|
-
const validCspDirectives = ["connect-src", "script-src", "style-src", "img-src", "font-src", "frame-src", "media-src"];
|
|
289
|
-
|
|
290
|
-
const hasAnyDirectives = Object.keys(cspExceptions).length > 0;
|
|
291
|
-
if (!hasAnyDirectives) {
|
|
292
|
-
errors.push("cspExceptions cannot be empty. If CSP exceptions are not needed, remove the cspExceptions field");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
Object.keys(cspExceptions).forEach((directive) => {
|
|
296
|
-
if (!validCspDirectives.includes(directive)) {
|
|
297
|
-
warnings.push(`Unknown CSP directive: ${directive}`);
|
|
298
|
-
}
|
|
299
|
-
const values = cspExceptions[/** @type {keyof CspExceptions} */ (directive)];
|
|
300
|
-
if (values && !Array.isArray(values)) {
|
|
301
|
-
errors.push(`CSP directive "${directive}" must be an array of strings`);
|
|
302
|
-
} else if (values && values.length === 0) {
|
|
303
|
-
errors.push(`CSP directive "${directive}" cannot be an empty array`);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Features validation (optional, but validated if present)
|
|
310
|
-
if (packageJson.features !== undefined) {
|
|
311
|
-
const features = packageJson.features;
|
|
312
|
-
|
|
313
|
-
if (features === null || typeof features !== "object" || Array.isArray(features)) {
|
|
314
|
-
errors.push("features must be a non-array object with optional 'multiConnection' and 'minAPI' properties");
|
|
315
|
-
} else {
|
|
316
|
-
const VALID_FEATURE_KEYS = ["multiConnection", "minAPI"];
|
|
317
|
-
const featureKeys = Object.keys(features);
|
|
318
|
-
const invalidKeys = featureKeys.filter((key) => !VALID_FEATURE_KEYS.includes(key));
|
|
319
|
-
|
|
320
|
-
if (invalidKeys.length > 0) {
|
|
321
|
-
errors.push(`features can only contain ${VALID_FEATURE_KEYS.map((k) => `'${k}'`).join(", ")} properties. Invalid properties: ${invalidKeys.join(", ")}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (features.multiConnection === undefined) {
|
|
325
|
-
errors.push("features.multiConnection is required when features object is provided");
|
|
326
|
-
} else if (!VALID_MULTI_CONNECTION_VALUES.includes(features.multiConnection)) {
|
|
327
|
-
errors.push(`features.multiConnection must be one of: ${VALID_MULTI_CONNECTION_VALUES.join(", ")}`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (features.minAPI !== undefined) {
|
|
331
|
-
if (typeof features.minAPI !== "string" || !SEMVER_REGEX.test(features.minAPI)) {
|
|
332
|
-
errors.push("features.minAPI must be a valid semantic version string (e.g., '1.0.0')");
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const valid = errors.length === 0;
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
valid,
|
|
342
|
-
errors,
|
|
343
|
-
warnings,
|
|
344
|
-
packageInfo: valid
|
|
345
|
-
? {
|
|
346
|
-
name: packageJson.name,
|
|
347
|
-
version: packageJson.version,
|
|
348
|
-
displayName: packageJson.displayName,
|
|
349
|
-
description: packageJson.description,
|
|
350
|
-
license: packageJson.license,
|
|
351
|
-
contributors: packageJson.contributors,
|
|
352
|
-
cspExceptions: packageJson.cspExceptions,
|
|
353
|
-
icon: packageJson.icon,
|
|
354
|
-
configurations: packageJson.configurations,
|
|
355
|
-
features: packageJson.features,
|
|
356
|
-
}
|
|
357
|
-
: undefined,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
module.exports = { validatePackageJson, validatePPTBConfig, isValidUrl, APPROVED_LICENSES, KNOWN_CAPABILITY_TAGS };
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Validates a tool's pptb.config.json against the official review criteria.
|
|
365
|
-
*
|
|
366
|
-
* @param {PPTBConfig} config - The parsed pptb.config.json object.
|
|
367
|
-
* @returns {ValidationResult}
|
|
368
|
-
*/
|
|
369
|
-
function validatePPTBConfig(config) {
|
|
370
|
-
const errors = /** @type {string[]} */ ([]);
|
|
371
|
-
const warnings = /** @type {string[]} */ ([]);
|
|
372
|
-
|
|
373
|
-
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
374
|
-
errors.push("pptb.config.json must be a JSON object");
|
|
375
|
-
return { valid: false, errors, warnings };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const VALID_ROOT_KEYS = ["invocation"];
|
|
379
|
-
const unknownRootKeys = Object.keys(config).filter((k) => !VALID_ROOT_KEYS.includes(k));
|
|
380
|
-
if (unknownRootKeys.length > 0) {
|
|
381
|
-
warnings.push(`pptb.config.json contains unrecognised root keys: ${unknownRootKeys.join(", ")}`);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Invocation section
|
|
385
|
-
if (config.invocation !== undefined) {
|
|
386
|
-
const inv = config.invocation;
|
|
387
|
-
|
|
388
|
-
if (inv === null || typeof inv !== "object" || Array.isArray(inv)) {
|
|
389
|
-
errors.push("invocation must be a non-array object");
|
|
390
|
-
} else {
|
|
391
|
-
// invocation.version – required, must be a valid semver string
|
|
392
|
-
if (inv.version === undefined || inv.version === null) {
|
|
393
|
-
errors.push("invocation.version is required");
|
|
394
|
-
} else if (typeof inv.version !== "string") {
|
|
395
|
-
errors.push("invocation.version must be a string");
|
|
396
|
-
} else if (!SEMVER_REGEX.test(inv.version)) {
|
|
397
|
-
errors.push(`invocation.version "${inv.version}" is not a valid semantic version string (e.g. "1.0.0")`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// invocation.prefill – optional JSON-schema-like object
|
|
401
|
-
if (inv.prefill !== undefined) {
|
|
402
|
-
if (inv.prefill === null || typeof inv.prefill !== "object" || Array.isArray(inv.prefill)) {
|
|
403
|
-
errors.push("invocation.prefill must be a non-array object");
|
|
404
|
-
} else if (inv.prefill.properties !== undefined) {
|
|
405
|
-
validateJsonSchemaProperties("invocation.prefill", inv.prefill.properties, errors);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// invocation.returnTopic – optional JSON-schema-like object
|
|
410
|
-
if (inv.returnTopic !== undefined) {
|
|
411
|
-
if (inv.returnTopic === null || typeof inv.returnTopic !== "object" || Array.isArray(inv.returnTopic)) {
|
|
412
|
-
errors.push("invocation.returnTopic must be a non-array object");
|
|
413
|
-
} else if (inv.returnTopic.properties !== undefined) {
|
|
414
|
-
validateJsonSchemaProperties("invocation.returnTopic", inv.returnTopic.properties, errors);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// invocation.capabilities – optional array of non-empty strings
|
|
419
|
-
if (inv.capabilities !== undefined) {
|
|
420
|
-
if (!Array.isArray(inv.capabilities)) {
|
|
421
|
-
errors.push("invocation.capabilities must be an array");
|
|
422
|
-
} else {
|
|
423
|
-
inv.capabilities.forEach((cap, idx) => {
|
|
424
|
-
if (typeof cap !== "string" || cap.trim().length === 0) {
|
|
425
|
-
errors.push(`invocation.capabilities[${idx}] must be a non-empty string`);
|
|
426
|
-
} else if (!KNOWN_CAPABILITY_TAGS.includes(cap.trim())) {
|
|
427
|
-
warnings.push(
|
|
428
|
-
`invocation.capabilities[${idx}] "${cap}" is not a recognised capability tag. ` +
|
|
429
|
-
`Known tags: ${KNOWN_CAPABILITY_TAGS.join(", ")}. ` +
|
|
430
|
-
"If this is a new tag, ensure it has been added to the capability registry.",
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const valid = errors.length === 0;
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
valid,
|
|
443
|
-
errors,
|
|
444
|
-
warnings,
|
|
445
|
-
packageInfo: valid
|
|
446
|
-
? {
|
|
447
|
-
invocation: config.invocation,
|
|
448
|
-
}
|
|
449
|
-
: undefined,
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Validates a JSON-schema-style `properties` map used inside invocation sections.
|
|
455
|
-
* Only performs basic structural validation; full JSON Schema validation is not required.
|
|
456
|
-
*
|
|
457
|
-
* @param {string} fieldName
|
|
458
|
-
* @param {unknown} properties
|
|
459
|
-
* @param {string[]} errors
|
|
460
|
-
*/
|
|
461
|
-
function validateJsonSchemaProperties(fieldName, properties, errors) {
|
|
462
|
-
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
463
|
-
errors.push(`${fieldName}.properties must be a non-array object`);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const propsRecord = /** @type {Record<string, unknown>} */ (properties);
|
|
468
|
-
|
|
469
|
-
for (const [key, value] of Object.entries(propsRecord)) {
|
|
470
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
471
|
-
errors.push(`${fieldName}.properties.${key} must be an object`);
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
const prop = /** @type {Record<string, unknown>} */ (value);
|
|
475
|
-
if (prop.type !== undefined && typeof prop.type !== "string") {
|
|
476
|
-
errors.push(`${fieldName}.properties.${key}.type must be a string`);
|
|
477
|
-
}
|
|
478
|
-
if (prop.enum !== undefined) {
|
|
479
|
-
if (!Array.isArray(prop.enum)) {
|
|
480
|
-
errors.push(`${fieldName}.properties.${key}.enum must be an array`);
|
|
481
|
-
} else if (prop.enum.length === 0) {
|
|
482
|
-
errors.push(`${fieldName}.properties.${key}.enum must not be empty`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|