@appstrate/validation 1.2.0 → 1.3.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/README.md CHANGED
@@ -37,11 +37,10 @@ if (result.valid) {
37
37
  }
38
38
  ```
39
39
 
40
- Three manifest schemas are available:
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 (63 tests across 5 files)
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 (63 tests, 5 files)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appstrate/validation",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
package/src/index.ts CHANGED
@@ -36,8 +36,11 @@ export const serviceRequirementSchema = z.looseObject({
36
36
  connectionMode: z.enum(["user", "admin"]).optional(),
37
37
  });
38
38
 
39
- const slugString = z.string().min(1).regex(SLUG_REGEX, {
40
- error: "Must be a valid slug (a-z, 0-9, hyphens, no leading/trailing hyphen)",
39
+ export const PACKAGE_REF_REGEX =
40
+ /^(?:@[a-z0-9]([a-z0-9-]*[a-z0-9])?\/)?[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
41
+
42
+ const packageRefString = z.string().min(1).regex(PACKAGE_REF_REGEX, {
43
+ error: "Must be a valid slug or scoped name (@scope/name)",
41
44
  });
42
45
 
43
46
  const semverRangeString = z.string().refine((val) => semver.validRange(val) !== null, {
@@ -59,12 +62,13 @@ export const scopedNameRegex = /^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\/[a-z0-9]([a-z0-
59
62
  export const packageTypeEnum = z.enum(["flow", "skill", "extension"]);
60
63
  export type PackageType = z.infer<typeof packageTypeEnum>;
61
64
 
62
- export const baseManifestSchema = z.looseObject({
65
+ export const manifestSchema = z.looseObject({
63
66
  name: z.string().regex(scopedNameRegex, { error: "Must follow the format @scope/package-name" }),
64
67
  version: z.string().refine((v) => semver.valid(v) !== null, {
65
68
  error: "Must be a valid semver version",
66
69
  }),
67
70
  type: packageTypeEnum,
71
+ displayName: z.string().optional().catch(undefined),
68
72
  description: z.string().optional().catch(undefined),
69
73
  keywords: z.array(z.string()).optional().catch(undefined),
70
74
  license: z.string().optional().catch(undefined),
@@ -73,7 +77,7 @@ export const baseManifestSchema = z.looseObject({
73
77
  registryDependencies: registryDependenciesSchema,
74
78
  });
75
79
 
76
- export type BaseManifest = z.infer<typeof baseManifestSchema>;
80
+ export type Manifest = z.infer<typeof manifestSchema>;
77
81
 
78
82
  // ─── Sub-schema inferred types ───────────────
79
83
 
@@ -88,8 +92,8 @@ export type FlowJsonSchemaObject = z.infer<typeof jsonSchemaObjectSchema>;
88
92
  const flowSharedFields = {
89
93
  requires: z.looseObject({
90
94
  services: z.array(serviceRequirementSchema),
91
- skills: z.array(slugString).optional(),
92
- extensions: z.array(slugString).optional(),
95
+ skills: z.array(packageRefString).optional(),
96
+ extensions: z.array(packageRefString).optional(),
93
97
  }),
94
98
  input: z
95
99
  .object({
@@ -118,7 +122,7 @@ const flowSharedFields = {
118
122
  // Flow manifest schema — extends base with flow-specific fields
119
123
  // ─────────────────────────────────────────────
120
124
 
121
- export const flowManifestSchema = baseManifestSchema.extend({
125
+ export const flowManifestSchema = manifestSchema.extend({
122
126
  $schema: z.string().optional(),
123
127
  schemaVersion: z.string(),
124
128
  displayName: z.string().min(1),
@@ -134,33 +138,14 @@ export type FlowManifest = z.infer<typeof flowManifestSchema>;
134
138
  // ─────────────────────────────────────────────
135
139
 
136
140
  export type ValidateManifestResult =
137
- | { valid: true; errors: []; manifest: BaseManifest | FlowManifest }
141
+ | { valid: true; errors: []; manifest: Manifest | FlowManifest }
138
142
  | { valid: false; errors: string[]; manifest?: undefined };
139
143
 
140
- export function validateManifest(raw: unknown): ValidateManifestResult {
141
- // First, check if it has a type field to dispatch
142
- if (raw && typeof raw === "object" && "type" in raw) {
143
- const obj = raw as Record<string, unknown>;
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 — require it explicitly
159
- return { valid: false, errors: ["type: Required field is missing"] };
160
- }
161
-
162
- function validateFlowManifest(raw: unknown): ValidateManifestResult {
163
- const result = flowManifestSchema.safeParse(raw);
144
+ function parseWithSchema(
145
+ schema: typeof manifestSchema | typeof flowManifestSchema,
146
+ raw: unknown,
147
+ ): ValidateManifestResult {
148
+ const result = schema.safeParse(raw);
164
149
  if (result.success) {
165
150
  return { valid: true, errors: [], manifest: result.data };
166
151
  }
@@ -168,6 +153,14 @@ function validateFlowManifest(raw: unknown): ValidateManifestResult {
168
153
  return { valid: false, errors };
169
154
  }
170
155
 
156
+ export function validateManifest(raw: unknown): ValidateManifestResult {
157
+ if (raw && typeof raw === "object" && "type" in raw) {
158
+ const schema = (raw as Record<string, unknown>).type === "flow" ? flowManifestSchema : manifestSchema;
159
+ return parseWithSchema(schema, raw);
160
+ }
161
+ return { valid: false, errors: ["type: Required field is missing"] };
162
+ }
163
+
171
164
  function stripQuotes(value: string): string {
172
165
  const trimmed = value.trim();
173
166
  if (
package/src/naming.ts CHANGED
@@ -9,13 +9,6 @@ export function stripScope(scope: string): string {
9
9
  return scope.startsWith("@") ? scope.slice(1) : scope;
10
10
  }
11
11
 
12
- /** Convert "@scope/name" to strate packageId "scope--name" */
13
- export function scopedNameToPackageId(scopedName: string): string {
14
- const match = scopedName.match(/^@([a-z0-9][a-z0-9-]*[a-z0-9]?)\/([a-z0-9][a-z0-9-]*[a-z0-9]?)$/);
15
- if (!match) throw new Error(`Invalid scoped package name: ${scopedName}`);
16
- return `${match[1]}--${match[2]}`;
17
- }
18
-
19
12
  /** Parse "@scope/name" into { scope, name } or null if invalid */
20
13
  export function parseScopedName(scopedName: string): { scope: string; name: string } | null {
21
14
  const match = scopedName.match(/^@([^/]+)\/(.+)$/);
@@ -23,17 +16,8 @@ export function parseScopedName(scopedName: string): { scope: string; name: stri
23
16
  return { scope: match[1]!, name: match[2]! };
24
17
  }
25
18
 
26
- /** Convert strate packageId "scope--name" to "@scope/name", or null for local slugs */
27
- export function packageIdToScopedName(packageId: string): string | null {
28
- const idx = packageId.indexOf("--");
29
- if (idx === -1) return null;
30
- const scope = packageId.slice(0, idx);
31
- const name = packageId.slice(idx + 2);
32
- if (!scope || !name) return null;
33
- return `@${scope}/${name}`;
34
- }
35
-
36
- /** Convert separated scope + name to strate packageId */
37
- export function depEntryToPackageId(depScope: string, depName: string): string {
38
- 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}`;
39
23
  }
package/src/zip.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { unzipSync, zipSync, type Zippable } from "fflate";
2
- import { validateManifest, extractSkillMeta, validateExtensionSource } from "./index.ts";
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: Record<string, unknown>;
71
+ manifest: Manifest | FlowManifest;
66
72
  content: string;
67
73
  files: Record<string, Uint8Array>;
68
74
  type: "flow" | "skill" | "extension";
@@ -133,9 +139,9 @@ export function parsePackageZip(zipBuffer: Uint8Array, maxSize?: number): Parsed
133
139
  );
134
140
  }
135
141
 
136
- const manifest = manifestRaw as Record<string, unknown>;
142
+ const manifest = validation.manifest!;
137
143
 
138
- const type = manifest.type as "flow" | "skill" | "extension";
144
+ const type = manifest.type;
139
145
 
140
146
  // Extract primary content based on type
141
147
  let content: string;