@appstrate/validation 1.1.1 → 1.3.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/README.md +3 -4
- package/package.json +1 -1
- package/src/index.ts +29 -73
- package/src/naming.ts +9 -18
- package/src/zip.ts +10 -5
package/README.md
CHANGED
|
@@ -37,11 +37,10 @@ if (result.valid) {
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Two manifest schemas are available:
|
|
41
41
|
|
|
42
42
|
- **`baseManifestSchema`** — Common fields for all types: `name` (`@scope/package-name`), `version` (semver), `type` (`flow` | `skill` | `extension`), optional `description`, `keywords`, `license`, `registryDependencies`
|
|
43
43
|
- **`flowManifestSchema`** — Extends base with flow-specific fields: `schemaVersion`, `displayName`, `author`, `requires` (services, skills, extensions), `input`/`output`/`config` schemas, `execution` options
|
|
44
|
-
- **`localFlowManifestSchema`** — Relaxed schema for strate built-in flows (slug name instead of scoped, optional version)
|
|
45
44
|
|
|
46
45
|
### Skill & Extension Validation
|
|
47
46
|
|
|
@@ -130,7 +129,7 @@ const sri = computeIntegrity(zipBuffer);
|
|
|
130
129
|
│ ├── dependencies.ts # extractDependencies, DepEntry type
|
|
131
130
|
│ └── integrity.ts # computeIntegrity (SHA256 SRI hash)
|
|
132
131
|
│
|
|
133
|
-
├── tests/ # bun:test (
|
|
132
|
+
├── tests/ # bun:test (66 tests across 5 files)
|
|
134
133
|
│ ├── index.test.ts # Manifest validation (flow, skill, extension, base)
|
|
135
134
|
│ ├── zip.test.ts # ZIP parsing, folder detection, error codes, security
|
|
136
135
|
│ ├── naming.test.ts # Naming helpers, packageId conversion
|
|
@@ -145,7 +144,7 @@ const sri = computeIntegrity(zipBuffer);
|
|
|
145
144
|
## Development
|
|
146
145
|
|
|
147
146
|
```sh
|
|
148
|
-
bun test # Run all tests (
|
|
147
|
+
bun test # Run all tests (66 tests, 5 files)
|
|
149
148
|
bun run check # TypeScript type-check + ESLint + Prettier
|
|
150
149
|
bun run lint # ESLint only
|
|
151
150
|
bun run format # Prettier format
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -26,14 +26,14 @@ export const jsonSchemaObjectSchema = z.object({
|
|
|
26
26
|
propertyOrder: z.array(z.string()).optional(),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
export const serviceRequirementSchema = z.
|
|
29
|
+
export const serviceRequirementSchema = z.looseObject({
|
|
30
30
|
id: z.string().min(1).regex(SLUG_REGEX, {
|
|
31
31
|
error: "Must be a valid slug (a-z, 0-9, hyphens, no leading/trailing hyphen)",
|
|
32
32
|
}),
|
|
33
33
|
provider: z.string(),
|
|
34
34
|
description: z.string().optional(),
|
|
35
|
-
scopes: z.array(z.string()).optional()
|
|
36
|
-
connectionMode: z.enum(["user", "admin"]).optional()
|
|
35
|
+
scopes: z.array(z.string()).optional(),
|
|
36
|
+
connectionMode: z.enum(["user", "admin"]).optional(),
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
const slugString = z.string().min(1).regex(SLUG_REGEX, {
|
|
@@ -45,9 +45,9 @@ const semverRangeString = z.string().refine((val) => semver.validRange(val) !==
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const registryDependenciesSchema = z
|
|
48
|
-
.
|
|
49
|
-
skills: z.record(z.string(), semverRangeString).optional()
|
|
50
|
-
extensions: z.record(z.string(), semverRangeString).optional()
|
|
48
|
+
.looseObject({
|
|
49
|
+
skills: z.record(z.string(), semverRangeString).optional(),
|
|
50
|
+
extensions: z.record(z.string(), semverRangeString).optional(),
|
|
51
51
|
})
|
|
52
52
|
.optional();
|
|
53
53
|
|
|
@@ -59,12 +59,13 @@ export const scopedNameRegex = /^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\/[a-z0-9]([a-z0-
|
|
|
59
59
|
export const packageTypeEnum = z.enum(["flow", "skill", "extension"]);
|
|
60
60
|
export type PackageType = z.infer<typeof packageTypeEnum>;
|
|
61
61
|
|
|
62
|
-
export const
|
|
62
|
+
export const manifestSchema = z.looseObject({
|
|
63
63
|
name: z.string().regex(scopedNameRegex, { error: "Must follow the format @scope/package-name" }),
|
|
64
64
|
version: z.string().refine((v) => semver.valid(v) !== null, {
|
|
65
65
|
error: "Must be a valid semver version",
|
|
66
66
|
}),
|
|
67
67
|
type: packageTypeEnum,
|
|
68
|
+
displayName: z.string().optional().catch(undefined),
|
|
68
69
|
description: z.string().optional().catch(undefined),
|
|
69
70
|
keywords: z.array(z.string()).optional().catch(undefined),
|
|
70
71
|
license: z.string().optional().catch(undefined),
|
|
@@ -73,7 +74,7 @@ export const baseManifestSchema = z.object({
|
|
|
73
74
|
registryDependencies: registryDependenciesSchema,
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
export type
|
|
77
|
+
export type Manifest = z.infer<typeof manifestSchema>;
|
|
77
78
|
|
|
78
79
|
// ─── Sub-schema inferred types ───────────────
|
|
79
80
|
|
|
@@ -82,14 +83,14 @@ export type FlowJsonSchemaProperty = z.infer<typeof jsonSchemaPropertySchema>;
|
|
|
82
83
|
export type FlowJsonSchemaObject = z.infer<typeof jsonSchemaObjectSchema>;
|
|
83
84
|
|
|
84
85
|
// ─────────────────────────────────────────────
|
|
85
|
-
// Shared flow fields
|
|
86
|
+
// Shared flow fields
|
|
86
87
|
// ─────────────────────────────────────────────
|
|
87
88
|
|
|
88
89
|
const flowSharedFields = {
|
|
89
|
-
requires: z.
|
|
90
|
+
requires: z.looseObject({
|
|
90
91
|
services: z.array(serviceRequirementSchema),
|
|
91
|
-
skills: z.array(slugString).optional()
|
|
92
|
-
extensions: z.array(slugString).optional()
|
|
92
|
+
skills: z.array(slugString).optional(),
|
|
93
|
+
extensions: z.array(slugString).optional(),
|
|
93
94
|
}),
|
|
94
95
|
input: z
|
|
95
96
|
.object({
|
|
@@ -107,7 +108,7 @@ const flowSharedFields = {
|
|
|
107
108
|
})
|
|
108
109
|
.optional(),
|
|
109
110
|
execution: z
|
|
110
|
-
.
|
|
111
|
+
.looseObject({
|
|
111
112
|
timeout: z.number().optional(),
|
|
112
113
|
outputRetries: z.number().min(0).max(5).optional(),
|
|
113
114
|
})
|
|
@@ -118,7 +119,7 @@ const flowSharedFields = {
|
|
|
118
119
|
// Flow manifest schema — extends base with flow-specific fields
|
|
119
120
|
// ─────────────────────────────────────────────
|
|
120
121
|
|
|
121
|
-
export const flowManifestSchema =
|
|
122
|
+
export const flowManifestSchema = manifestSchema.extend({
|
|
122
123
|
$schema: z.string().optional(),
|
|
123
124
|
schemaVersion: z.string(),
|
|
124
125
|
displayName: z.string().min(1),
|
|
@@ -134,34 +135,14 @@ export type FlowManifest = z.infer<typeof flowManifestSchema>;
|
|
|
134
135
|
// ─────────────────────────────────────────────
|
|
135
136
|
|
|
136
137
|
export type ValidateManifestResult =
|
|
137
|
-
| { valid: true; errors: []; manifest:
|
|
138
|
+
| { valid: true; errors: []; manifest: Manifest | FlowManifest }
|
|
138
139
|
| { valid: false; errors: string[]; manifest?: undefined };
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (obj.type === "flow") {
|
|
146
|
-
return validateFlowManifest(raw);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// For skill/extension, validate with base schema only
|
|
150
|
-
const result = baseManifestSchema.safeParse(raw);
|
|
151
|
-
if (result.success) {
|
|
152
|
-
return { valid: true, errors: [], manifest: result.data };
|
|
153
|
-
}
|
|
154
|
-
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
155
|
-
return { valid: false, errors };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// No type field — try flow manifest validation (backward compat for flow-only manifests
|
|
159
|
-
// that don't have name/version/type because they rely on the old format)
|
|
160
|
-
return validateFlowManifest(raw);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function validateFlowManifest(raw: unknown): ValidateManifestResult {
|
|
164
|
-
const result = flowManifestSchema.safeParse(raw);
|
|
141
|
+
function parseWithSchema(
|
|
142
|
+
schema: typeof manifestSchema | typeof flowManifestSchema,
|
|
143
|
+
raw: unknown,
|
|
144
|
+
): ValidateManifestResult {
|
|
145
|
+
const result = schema.safeParse(raw);
|
|
165
146
|
if (result.success) {
|
|
166
147
|
return { valid: true, errors: [], manifest: result.data };
|
|
167
148
|
}
|
|
@@ -169,6 +150,14 @@ function validateFlowManifest(raw: unknown): ValidateManifestResult {
|
|
|
169
150
|
return { valid: false, errors };
|
|
170
151
|
}
|
|
171
152
|
|
|
153
|
+
export function validateManifest(raw: unknown): ValidateManifestResult {
|
|
154
|
+
if (raw && typeof raw === "object" && "type" in raw) {
|
|
155
|
+
const schema = (raw as Record<string, unknown>).type === "flow" ? flowManifestSchema : manifestSchema;
|
|
156
|
+
return parseWithSchema(schema, raw);
|
|
157
|
+
}
|
|
158
|
+
return { valid: false, errors: ["type: Required field is missing"] };
|
|
159
|
+
}
|
|
160
|
+
|
|
172
161
|
function stripQuotes(value: string): string {
|
|
173
162
|
const trimmed = value.trim();
|
|
174
163
|
if (
|
|
@@ -233,39 +222,6 @@ function countParams(paramStr: string): number {
|
|
|
233
222
|
return count;
|
|
234
223
|
}
|
|
235
224
|
|
|
236
|
-
// ─────────────────────────────────────────────
|
|
237
|
-
// Local flow manifest schema — relaxed for strate (no scoped name, optional version)
|
|
238
|
-
// ─────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
export const localFlowManifestSchema = z.looseObject({
|
|
241
|
-
$schema: z.string().optional(),
|
|
242
|
-
schemaVersion: z.string(),
|
|
243
|
-
name: slugString,
|
|
244
|
-
displayName: z.string().min(1),
|
|
245
|
-
description: z.string(),
|
|
246
|
-
author: z.string(),
|
|
247
|
-
license: z.string().optional(),
|
|
248
|
-
tags: z.array(z.string()).optional(),
|
|
249
|
-
type: z.string().optional(),
|
|
250
|
-
version: z.string().optional(),
|
|
251
|
-
...flowSharedFields,
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
export type LocalFlowManifest = z.infer<typeof localFlowManifestSchema>;
|
|
255
|
-
|
|
256
|
-
export function validateLocalFlowManifest(raw: unknown): {
|
|
257
|
-
valid: boolean;
|
|
258
|
-
errors: string[];
|
|
259
|
-
manifest?: unknown;
|
|
260
|
-
} {
|
|
261
|
-
const result = localFlowManifestSchema.safeParse(raw);
|
|
262
|
-
if (result.success) {
|
|
263
|
-
return { valid: true, errors: [], manifest: result.data };
|
|
264
|
-
}
|
|
265
|
-
const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
266
|
-
return { valid: false, errors };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
225
|
export function validateExtensionSource(source: string): ExtensionValidationResult {
|
|
270
226
|
const errors: string[] = [];
|
|
271
227
|
const warnings: string[] = [];
|
package/src/naming.ts
CHANGED
|
@@ -9,24 +9,15 @@ export function stripScope(scope: string): string {
|
|
|
9
9
|
return scope.startsWith("@") ? scope.slice(1) : scope;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
export function
|
|
14
|
-
const match = scopedName.match(/^@([
|
|
15
|
-
if (!match)
|
|
16
|
-
return
|
|
12
|
+
/** Parse "@scope/name" into { scope, name } or null if invalid */
|
|
13
|
+
export function parseScopedName(scopedName: string): { scope: string; name: string } | null {
|
|
14
|
+
const match = scopedName.match(/^@([^/]+)\/(.+)$/);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
return { scope: match[1]!, name: match[2]! };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export function
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const scope = packageId.slice(0, idx);
|
|
24
|
-
const name = packageId.slice(idx + 2);
|
|
25
|
-
if (!scope || !name) return null;
|
|
26
|
-
return `@${scope}/${name}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Convert separated scope + name to strate packageId */
|
|
30
|
-
export function depEntryToPackageId(depScope: string, depName: string): string {
|
|
31
|
-
return `${stripScope(depScope)}--${depName}`;
|
|
19
|
+
/** Build a packageId from separated scope + name. */
|
|
20
|
+
export function buildPackageId(scope: string, name: string): string {
|
|
21
|
+
const s = stripScope(scope);
|
|
22
|
+
return `@${s}/${name}`;
|
|
32
23
|
}
|
package/src/zip.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { unzipSync, zipSync, type Zippable } from "fflate";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
validateManifest,
|
|
4
|
+
extractSkillMeta,
|
|
5
|
+
validateExtensionSource,
|
|
6
|
+
type Manifest,
|
|
7
|
+
type FlowManifest,
|
|
8
|
+
} from "./index.ts";
|
|
3
9
|
|
|
4
10
|
export type { Zippable };
|
|
5
11
|
|
|
@@ -62,7 +68,7 @@ function getFileText(
|
|
|
62
68
|
// ─────────────────────────────────────────────
|
|
63
69
|
|
|
64
70
|
export interface ParsedPackageZip {
|
|
65
|
-
manifest:
|
|
71
|
+
manifest: Manifest | FlowManifest;
|
|
66
72
|
content: string;
|
|
67
73
|
files: Record<string, Uint8Array>;
|
|
68
74
|
type: "flow" | "skill" | "extension";
|
|
@@ -133,10 +139,9 @@ export function parsePackageZip(zipBuffer: Uint8Array, maxSize?: number): Parsed
|
|
|
133
139
|
);
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
const manifest = validation.manifest
|
|
142
|
+
const manifest = validation.manifest!;
|
|
137
143
|
|
|
138
|
-
|
|
139
|
-
const type = (manifest.type as "flow" | "skill" | "extension") ?? "flow";
|
|
144
|
+
const type = manifest.type;
|
|
140
145
|
|
|
141
146
|
// Extract primary content based on type
|
|
142
147
|
let content: string;
|