@pptb/types 1.1.3-beta.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -24
  2. package/lib/validate.js +69 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,11 +4,11 @@ TypeScript type definitions for Power Platform ToolBox APIs, plus a built-in CLI
4
4
 
5
5
  - [@pptb/types](#pptbtypes)
6
6
  - [Installation](#installation)
7
- - [Overview](#overview)
8
7
  - [Tool Validation](#tool-validation)
9
8
  - [Quick start](#quick-start)
10
9
  - [CLI options](#cli-options)
11
10
  - [What is validated](#what-is-validated)
11
+ - [Overview](#overview)
12
12
  - [Usage](#usage)
13
13
  - [Include all type definitions](#include-all-type-definitions)
14
14
  - [Include specific API types](#include-specific-api-types)
@@ -55,7 +55,7 @@ Add a script to your tool's `package.json`:
55
55
  ```json
56
56
  {
57
57
  "scripts": {
58
- "pptb-validate": "pptb-validate"
58
+ "validate": "pptb-validate"
59
59
  }
60
60
  }
61
61
  ```
@@ -63,7 +63,7 @@ Add a script to your tool's `package.json`:
63
63
  Then run:
64
64
 
65
65
  ```bash
66
- npm run pptb-validate
66
+ npm run validate
67
67
  ```
68
68
 
69
69
  You can also run it directly (no script entry needed once `@pptb/types` is installed):
@@ -80,32 +80,32 @@ npx pptb-validate path/to/package.json
80
80
 
81
81
  ### CLI options
82
82
 
83
- | Option | Description |
84
- |---|---|
85
- | `--skip-url-checks` | Skip URL reachability checks (faster, works offline) |
86
- | `--json` | Print results as a JSON object (suitable for CI pipelines) |
87
- | `--help`, `-h` | Show help information |
83
+ | Option | Description |
84
+ | ------------------- | ---------------------------------------------------------- |
85
+ | `--skip-url-checks` | Skip URL reachability checks (faster, works offline) |
86
+ | `--json` | Print results as a JSON object (suitable for CI pipelines) |
87
+ | `--help`, `-h` | Show help information |
88
88
 
89
89
  ### What is validated
90
90
 
91
91
  The validator checks every field that the official review pipeline inspects:
92
92
 
93
- | Field | Required | Rules |
94
- |---|---|---|
95
- | `name` | ✅ | Must be a string |
96
- | `version` | ✅ | Must be a string |
97
- | `displayName` | ✅ | Must be a string |
98
- | `description` | ✅ | Must be a string |
99
- | `license` | ✅ | Must be one of the approved OSS 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) |
100
- | `contributors` | ✅ | Non-empty array; each entry needs a `name` |
101
- | `configurations.repository` | ✅ | Valid, reachable URL |
102
- | `configurations.readmeUrl` | ✅ | Valid URL; must **not** be hosted on `github.com` (use `raw.githubusercontent.com`) |
103
- | `configurations.website` | ❌ | Valid, reachable URL when provided |
104
- | `configurations.funding` | ❌ | Valid, reachable URL when provided |
105
- | `icon` | ❌ | Relative path to a `.svg` file bundled under `dist/`; must not be an HTTP URL or an absolute path |
106
- | `cspExceptions` | ❌ | When present: must not be empty; only recognised directives; each directive must be a non-empty array |
107
- | `features.multiConnection` | ❌* | Required when `features` is present; must be `"required"`, `"optional"`, or `"none"` |
108
- | `features.minAPI` | ❌ | Valid semver string when provided |
93
+ | Field | Required | Rules |
94
+ | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `name` | ✅ | Must be a string |
96
+ | `version` | ✅ | Must be a string |
97
+ | `displayName` | ✅ | Must be a string |
98
+ | `description` | ✅ | Must be a string |
99
+ | `license` | ✅ | Must be one of the approved OSS 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) |
100
+ | `contributors` | ✅ | Non-empty array; each entry needs a `name` |
101
+ | `configurations.repository` | ✅ | Valid, reachable URL |
102
+ | `configurations.readmeUrl` | ✅ | Valid URL; must **not** be hosted on `github.com` (use `raw.githubusercontent.com`) |
103
+ | `configurations.website` | ❌ | Valid, reachable URL when provided |
104
+ | `configurations.funding` | ❌ | Valid, reachable URL when provided |
105
+ | `icon` | ❌ | Relative path to a `.svg` file bundled under `dist/`; must not be an HTTP URL or an absolute path |
106
+ | `cspExceptions` | ❌ | When present: must not be empty; only recognised directives; each directive must be a non-empty array |
107
+ | `features.multiConnection` | ❌\* | Required when `features` is present; must be `"required"`, `"optional"`, or `"none"` |
108
+ | `features.minAPI` | ❌ | Valid semver string when provided |
109
109
 
110
110
  > \* Required only when the `features` object is present.
111
111
 
package/lib/validate.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
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[] }} CspExceptions */
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
11
  /** @typedef {{ repository?: string; website?: string; funding?: string; readmeUrl?: string }} Configurations */
12
12
  /** @typedef {{ multiConnection?: "required" | "optional" | "none"; minAPI?: string }} Features */
13
13
  /**
@@ -85,6 +85,16 @@ function validateIconPath(fieldName, iconPath, errors) {
85
85
  errors.push(`${fieldName} must be a relative path (e.g., 'icon.svg' or 'icons/icon.svg')`);
86
86
  return;
87
87
  }
88
+ // Reject Windows absolute paths like "C:\..." or "C:/"
89
+ if (/^[a-zA-Z]:/.test(iconPath)) {
90
+ errors.push(`${fieldName} must be a relative path (e.g., 'icon.svg' or 'icons/icon.svg')`);
91
+ return;
92
+ }
93
+ // Reject backslashes — icon paths must use forward slashes
94
+ if (iconPath.includes("\\")) {
95
+ errors.push(`${fieldName} must use forward slashes, not backslashes (e.g., 'icons/icon.svg')`);
96
+ return;
97
+ }
88
98
  if (iconPath.includes("..")) {
89
99
  errors.push(`${fieldName} cannot contain '..' (path traversal not allowed)`);
90
100
  return;
@@ -145,12 +155,12 @@ async function validatePackageJson(packageJson, options = {}) {
145
155
  }
146
156
 
147
157
  // Icon validation (optional, but must be a relative SVG path if provided)
148
- if (packageJson.icon !== undefined && packageJson.icon !== null) {
149
- if (typeof packageJson.icon !== "string") {
150
- errors.push("icon must be a string (relative path to bundled SVG under dist)");
151
- } else {
152
- validateIconPath("icon", packageJson.icon, errors);
153
- }
158
+ if (packageJson.icon === undefined || packageJson.icon === null) {
159
+ warnings.push("icon is not set; consider adding a bundled SVG icon so your tool displays properly in the marketplace");
160
+ } else if (typeof packageJson.icon !== "string") {
161
+ errors.push("icon must be a string (relative path to bundled SVG under dist)");
162
+ } else {
163
+ validateIconPath("icon", packageJson.icon, errors);
154
164
  }
155
165
 
156
166
  // Contributors validation
@@ -192,19 +202,19 @@ async function validatePackageJson(packageJson, options = {}) {
192
202
  }
193
203
  }
194
204
 
195
- // Website validation (optional)
196
- if (configs.website) {
197
- if (!isValidUrl(configs.website)) {
198
- warnings.push("configurations.website has an invalid URL format");
199
- } else if (!skipUrlChecks) {
200
- const accessible = await isUrlAccessible(configs.website);
201
- if (!accessible) {
202
- warnings.push("configurations.website URL is not accessible");
203
- }
205
+ // Website validation (optional but recommended)
206
+ if (!configs.website) {
207
+ warnings.push("configurations.website is not set; consider adding a URL where users can learn more about your tool");
208
+ } else if (!isValidUrl(configs.website)) {
209
+ warnings.push("configurations.website has an invalid URL format");
210
+ } else if (!skipUrlChecks) {
211
+ const accessible = await isUrlAccessible(configs.website);
212
+ if (!accessible) {
213
+ warnings.push("configurations.website URL is not accessible");
204
214
  }
205
215
  }
206
216
 
207
- // Funding validation (optional)
217
+ // Funding validation (optional but recommended)
208
218
  if (configs.funding) {
209
219
  if (!isValidUrl(configs.funding)) {
210
220
  warnings.push("configurations.funding has an invalid URL format");
@@ -233,45 +243,57 @@ async function validatePackageJson(packageJson, options = {}) {
233
243
 
234
244
  // CSP Exceptions validation (optional, but validated if present)
235
245
  if (packageJson.cspExceptions) {
236
- const validCspDirectives = ["connect-src", "script-src", "style-src", "img-src", "font-src", "frame-src"];
246
+ const cspExceptions = packageJson.cspExceptions;
237
247
 
238
- const hasAnyDirectives = Object.keys(packageJson.cspExceptions).length > 0;
239
- if (!hasAnyDirectives) {
240
- errors.push("cspExceptions cannot be empty. If CSP exceptions are not needed, remove the cspExceptions field");
241
- }
248
+ if (typeof cspExceptions !== "object" || cspExceptions === null || Array.isArray(cspExceptions)) {
249
+ errors.push("cspExceptions must be an object mapping CSP directives to arrays of strings");
250
+ } else {
251
+ const validCspDirectives = ["connect-src", "script-src", "style-src", "img-src", "font-src", "frame-src", "media-src"];
242
252
 
243
- Object.keys(packageJson.cspExceptions).forEach((directive) => {
244
- if (!validCspDirectives.includes(directive)) {
245
- warnings.push(`Unknown CSP directive: ${directive}`);
246
- }
247
- const values = packageJson.cspExceptions?.[/** @type {keyof CspExceptions} */ (directive)];
248
- if (values && !Array.isArray(values)) {
249
- errors.push(`CSP directive "${directive}" must be an array of strings`);
250
- } else if (values && values.length === 0) {
251
- errors.push(`CSP directive "${directive}" cannot be an empty array`);
253
+ const hasAnyDirectives = Object.keys(cspExceptions).length > 0;
254
+ if (!hasAnyDirectives) {
255
+ errors.push("cspExceptions cannot be empty. If CSP exceptions are not needed, remove the cspExceptions field");
252
256
  }
253
- });
257
+
258
+ Object.keys(cspExceptions).forEach((directive) => {
259
+ if (!validCspDirectives.includes(directive)) {
260
+ warnings.push(`Unknown CSP directive: ${directive}`);
261
+ }
262
+ const values = cspExceptions[/** @type {keyof CspExceptions} */ (directive)];
263
+ if (values && !Array.isArray(values)) {
264
+ errors.push(`CSP directive "${directive}" must be an array of strings`);
265
+ } else if (values && values.length === 0) {
266
+ errors.push(`CSP directive "${directive}" cannot be an empty array`);
267
+ }
268
+ });
269
+ }
254
270
  }
255
271
 
256
272
  // Features validation (optional, but validated if present)
257
- if (packageJson.features) {
258
- const VALID_FEATURE_KEYS = ["multiConnection", "minAPI"];
259
- const featureKeys = Object.keys(packageJson.features);
260
- const invalidKeys = featureKeys.filter((key) => !VALID_FEATURE_KEYS.includes(key));
273
+ if (packageJson.features !== undefined) {
274
+ const features = packageJson.features;
261
275
 
262
- if (invalidKeys.length > 0) {
263
- errors.push(`features can only contain ${VALID_FEATURE_KEYS.map((k) => `'${k}'`).join(", ")} properties. Invalid properties: ${invalidKeys.join(", ")}`);
264
- }
276
+ if (features === null || typeof features !== "object" || Array.isArray(features)) {
277
+ errors.push("features must be a non-array object with optional 'multiConnection' and 'minAPI' properties");
278
+ } else {
279
+ const VALID_FEATURE_KEYS = ["multiConnection", "minAPI"];
280
+ const featureKeys = Object.keys(features);
281
+ const invalidKeys = featureKeys.filter((key) => !VALID_FEATURE_KEYS.includes(key));
265
282
 
266
- if (packageJson.features.multiConnection === undefined) {
267
- errors.push("features.multiConnection is required when features object is provided");
268
- } else if (!VALID_MULTI_CONNECTION_VALUES.includes(packageJson.features.multiConnection)) {
269
- errors.push(`features.multiConnection must be one of: ${VALID_MULTI_CONNECTION_VALUES.join(", ")}`);
270
- }
283
+ if (invalidKeys.length > 0) {
284
+ errors.push(`features can only contain ${VALID_FEATURE_KEYS.map((k) => `'${k}'`).join(", ")} properties. Invalid properties: ${invalidKeys.join(", ")}`);
285
+ }
271
286
 
272
- if (packageJson.features.minAPI !== undefined) {
273
- if (typeof packageJson.features.minAPI !== "string" || !SEMVER_REGEX.test(packageJson.features.minAPI)) {
274
- errors.push("features.minAPI must be a valid semantic version string (e.g., '1.0.0')");
287
+ if (features.multiConnection === undefined) {
288
+ errors.push("features.multiConnection is required when features object is provided");
289
+ } else if (!VALID_MULTI_CONNECTION_VALUES.includes(features.multiConnection)) {
290
+ errors.push(`features.multiConnection must be one of: ${VALID_MULTI_CONNECTION_VALUES.join(", ")}`);
291
+ }
292
+
293
+ if (features.minAPI !== undefined) {
294
+ if (typeof features.minAPI !== "string" || !SEMVER_REGEX.test(features.minAPI)) {
295
+ errors.push("features.minAPI must be a valid semantic version string (e.g., '1.0.0')");
296
+ }
275
297
  }
276
298
  }
277
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pptb/types",
3
- "version": "1.1.3-beta.1",
3
+ "version": "1.2.0",
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",