@afps-spec/schema 1.0.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afps-spec/schema",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -11,12 +11,17 @@
11
11
  "./v1/*": "./v1/*"
12
12
  },
13
13
  "scripts": {
14
- "generate": "bun src/generate.ts"
14
+ "generate": "bun src/generate.ts",
15
+ "check": "bun src/generate.ts --check",
16
+ "test": "bun test",
17
+ "test:conformance": "bun test tests/conformance.test.ts"
15
18
  },
16
19
  "dependencies": {
20
+ "semver": "^7.7.4",
17
21
  "zod": "^4.3.6"
18
22
  },
19
23
  "devDependencies": {
24
+ "@types/semver": "^7.7.1",
20
25
  "bun-types": "latest"
21
26
  }
22
27
  }
package/src/generate.ts CHANGED
@@ -3,12 +3,14 @@
3
3
  *
4
4
  * Change MAJOR to generate schemas for a different spec version.
5
5
  *
6
- * Usage: bun src/generate.ts (from schema/)
6
+ * Usage:
7
+ * bun src/generate.ts Generate/update JSON schemas
8
+ * bun src/generate.ts --check Verify committed schemas match Zod source (CI)
7
9
  */
8
10
 
9
11
  import { toJSONSchema } from "zod/v4/core";
10
12
  import { resolve, dirname } from "node:path";
11
- import { writeFile, mkdir } from "node:fs/promises";
13
+ import { writeFile, mkdir, readFile } from "node:fs/promises";
12
14
  import { createSchemas } from "./schemas.ts";
13
15
 
14
16
  const MAJOR = 1;
@@ -16,6 +18,8 @@ const VERSION_TAG = `v${MAJOR}`;
16
18
  const BASE_URL = "https://afps.appstrate.dev/schema";
17
19
  const OUTPUT_DIR = resolve(dirname(import.meta.filename!), "..", VERSION_TAG);
18
20
 
21
+ const isCheck = process.argv.includes("--check");
22
+
19
23
  const { flowManifestSchema, skillManifestSchema, toolManifestSchema, providerManifestSchema } =
20
24
  createSchemas(MAJOR);
21
25
 
@@ -54,7 +58,11 @@ const entries = [
54
58
  },
55
59
  ];
56
60
 
57
- await mkdir(OUTPUT_DIR, { recursive: true });
61
+ if (!isCheck) {
62
+ await mkdir(OUTPUT_DIR, { recursive: true });
63
+ }
64
+
65
+ let mismatch = false;
58
66
 
59
67
  for (const entry of entries) {
60
68
  const jsonSchema = toJSONSchema(entry.schema, {
@@ -72,9 +80,36 @@ for (const entry of entries) {
72
80
  ...jsonSchema,
73
81
  };
74
82
 
83
+ const generated = JSON.stringify(final, null, 2) + "\n";
75
84
  const filePath = resolve(OUTPUT_DIR, entry.filename);
76
- await writeFile(filePath, JSON.stringify(final, null, 2) + "\n");
77
- console.log(` ✓ ${VERSION_TAG}/${entry.filename}`);
85
+
86
+ if (isCheck) {
87
+ let committed: string;
88
+ try {
89
+ committed = await readFile(filePath, "utf-8");
90
+ } catch {
91
+ console.error(` ✗ ${VERSION_TAG}/${entry.filename} — file missing`);
92
+ mismatch = true;
93
+ continue;
94
+ }
95
+ if (committed !== generated) {
96
+ console.error(` ✗ ${VERSION_TAG}/${entry.filename} — out of date`);
97
+ mismatch = true;
98
+ } else {
99
+ console.log(` ✓ ${VERSION_TAG}/${entry.filename}`);
100
+ }
101
+ } else {
102
+ await writeFile(filePath, generated);
103
+ console.log(` ✓ ${VERSION_TAG}/${entry.filename}`);
104
+ }
78
105
  }
79
106
 
80
- console.log(`\nGenerated ${entries.length} schemas in ${OUTPUT_DIR}`);
107
+ if (isCheck) {
108
+ if (mismatch) {
109
+ console.error("\nJSON schemas are out of date. Run `bun run generate` to update.");
110
+ process.exit(1);
111
+ }
112
+ console.log("\nAll JSON schemas are up to date.");
113
+ } else {
114
+ console.log(`\nGenerated ${entries.length} schemas in ${OUTPUT_DIR}`);
115
+ }
package/src/index.ts CHANGED
@@ -11,4 +11,11 @@ export {
11
11
  skillManifestSchema,
12
12
  toolManifestSchema,
13
13
  providerManifestSchema,
14
+ // Shared sub-schemas — reusable by consumers
15
+ authModeEnum,
16
+ providerDefinition,
17
+ providerConfiguration,
18
+ setupGuide,
19
+ schemaProperty,
20
+ schemaObject,
14
21
  } from "./schemas.ts";
package/src/schemas.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { z } from "zod";
14
+ import semver from "semver";
14
15
 
15
16
  // ─────────────────────────────────────────────
16
17
  // Shared patterns and primitives
@@ -19,8 +20,17 @@ import { z } from "zod";
19
20
  const SLUG_PATTERN = "[a-z0-9]([a-z0-9-]*[a-z0-9])?";
20
21
  const SCOPED_NAME_REGEX = new RegExp(`^@${SLUG_PATTERN}\\/${SLUG_PATTERN}$`);
21
22
 
22
- const scopedName = z.string().regex(SCOPED_NAME_REGEX);
23
- const semverRange = z.string().min(1);
23
+ const scopedName = z.string().regex(SCOPED_NAME_REGEX, {
24
+ error: "Must follow @scope/name format",
25
+ });
26
+
27
+ const semverVersion = z.string().refine((v) => semver.valid(v) !== null, {
28
+ error: "Must be a valid semver version (e.g. 1.0.0)",
29
+ });
30
+
31
+ const semverRange = z.string().refine((v) => semver.validRange(v) !== null, {
32
+ error: "Must be a valid semver range (e.g. ^1.0.0, ~2.1, >=3.0.0)",
33
+ });
24
34
 
25
35
  // ─────────────────────────────────────────────
26
36
  // Schema system (§5)
@@ -28,7 +38,7 @@ const semverRange = z.string().min(1);
28
38
 
29
39
  const fieldTypeEnum = z.enum(["string", "number", "boolean", "array", "object", "file"]);
30
40
 
31
- const schemaProperty = z.looseObject({
41
+ export const schemaProperty = z.looseObject({
32
42
  type: fieldTypeEnum,
33
43
  description: z.string().optional(),
34
44
  default: z.unknown().optional(),
@@ -38,10 +48,10 @@ const schemaProperty = z.looseObject({
38
48
  accept: z.string().optional(),
39
49
  maxSize: z.number().positive().optional(),
40
50
  multiple: z.boolean().optional(),
41
- maxFiles: z.number().positive().optional(),
51
+ maxFiles: z.number().int().positive().optional(),
42
52
  });
43
53
 
44
- const schemaObject = z.looseObject({
54
+ export const schemaObject = z.looseObject({
45
55
  type: z.literal("object"),
46
56
  properties: z.record(z.string(), schemaProperty),
47
57
  required: z.array(z.string()).optional(),
@@ -68,7 +78,7 @@ const dependenciesSchema = z
68
78
  // Provider configuration (§4.4)
69
79
  // ─────────────────────────────────────────────
70
80
 
71
- const providerConfiguration = z.looseObject({
81
+ export const providerConfiguration = z.looseObject({
72
82
  scopes: z.array(z.string()).optional(),
73
83
  connectionMode: z.enum(["user", "admin"]).optional(),
74
84
  });
@@ -87,9 +97,9 @@ const toolInterface = z.object({
87
97
  // Auth and provider internals (§3.5 + §7)
88
98
  // ─────────────────────────────────────────────
89
99
 
90
- const authModeEnum = z.enum(["oauth2", "oauth1", "api_key", "basic", "custom"]);
100
+ export const authModeEnum = z.enum(["oauth2", "oauth1", "api_key", "basic", "custom"]);
91
101
 
92
- const providerDefinition = z.looseObject({
102
+ export const providerDefinition = z.looseObject({
93
103
  authMode: authModeEnum,
94
104
  authorizationUrl: z.string().optional(),
95
105
  tokenUrl: z.string().optional(),
@@ -111,7 +121,7 @@ const providerDefinition = z.looseObject({
111
121
  availableScopes: z.array(z.unknown()).optional(),
112
122
  });
113
123
 
114
- const setupGuide = z
124
+ export const setupGuide = z
115
125
  .object({
116
126
  callbackUrlHint: z.string().optional(),
117
127
  steps: z
@@ -132,11 +142,13 @@ const setupGuide = z
132
142
  export function createSchemas(majorVersion: number) {
133
143
  const schemaVersionField = z
134
144
  .string()
135
- .regex(new RegExp(`^${majorVersion}\\.(0|[1-9]\\d*)$`));
145
+ .regex(new RegExp(`^${majorVersion}\\.(0|[1-9]\\d*)$`), {
146
+ error: `Must follow MAJOR.MINOR format (e.g. "${majorVersion}.0")`,
147
+ });
136
148
 
137
149
  const commonFields = {
138
150
  name: scopedName,
139
- version: z.string().min(1),
151
+ version: semverVersion,
140
152
  type: z.enum(["flow", "skill", "tool", "provider"]),
141
153
  displayName: z.string().optional(),
142
154
  description: z.string().optional(),
@@ -152,7 +164,7 @@ export function createSchemas(majorVersion: number) {
152
164
  type: z.literal("flow"),
153
165
  schemaVersion: schemaVersionField,
154
166
  displayName: z.string().min(1),
155
- author: z.string(),
167
+ author: z.string().min(1),
156
168
  providersConfiguration: z.record(scopedName, providerConfiguration).optional(),
157
169
  input: schemaWrapper.optional(),
158
170
  output: schemaWrapper.optional(),
@@ -172,15 +184,58 @@ export function createSchemas(majorVersion: number) {
172
184
  tool: toolInterface,
173
185
  });
174
186
 
175
- const providerManifestSchema = z.looseObject({
176
- ...commonFields,
177
- type: z.literal("provider"),
178
- iconUrl: z.string().optional(),
179
- categories: z.array(z.string()).optional(),
180
- docsUrl: z.string().optional(),
181
- definition: providerDefinition,
182
- setupGuide: setupGuide,
183
- });
187
+ const providerManifestSchema = z
188
+ .looseObject({
189
+ ...commonFields,
190
+ type: z.literal("provider"),
191
+ iconUrl: z.string().optional(),
192
+ categories: z.array(z.string()).optional(),
193
+ docsUrl: z.string().optional(),
194
+ definition: providerDefinition,
195
+ setupGuide: setupGuide,
196
+ })
197
+ .superRefine((val, ctx) => {
198
+ const mode = val.definition?.authMode;
199
+ if (mode === "oauth2") {
200
+ if (!val.definition.authorizationUrl) {
201
+ ctx.addIssue({
202
+ code: "custom",
203
+ path: ["definition", "authorizationUrl"],
204
+ message: "Required for oauth2 authMode",
205
+ });
206
+ }
207
+ if (!val.definition.tokenUrl) {
208
+ ctx.addIssue({
209
+ code: "custom",
210
+ path: ["definition", "tokenUrl"],
211
+ message: "Required for oauth2 authMode",
212
+ });
213
+ }
214
+ } else if (mode === "oauth1") {
215
+ if (!val.definition.requestTokenUrl) {
216
+ ctx.addIssue({
217
+ code: "custom",
218
+ path: ["definition", "requestTokenUrl"],
219
+ message: "Required for oauth1 authMode",
220
+ });
221
+ }
222
+ if (!val.definition.accessTokenUrl) {
223
+ ctx.addIssue({
224
+ code: "custom",
225
+ path: ["definition", "accessTokenUrl"],
226
+ message: "Required for oauth1 authMode",
227
+ });
228
+ }
229
+ } else if (mode === "api_key" || mode === "basic" || mode === "custom") {
230
+ if (!val.definition.credentialSchema) {
231
+ ctx.addIssue({
232
+ code: "custom",
233
+ path: ["definition", "credentialSchema"],
234
+ message: `Required for ${mode} authMode`,
235
+ });
236
+ }
237
+ }
238
+ });
184
239
 
185
240
  return { flowManifestSchema, skillManifestSchema, toolManifestSchema, providerManifestSchema };
186
241
  }
@@ -10,8 +10,7 @@
10
10
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
11
11
  },
12
12
  "version": {
13
- "type": "string",
14
- "minLength": 1
13
+ "type": "string"
15
14
  },
16
15
  "type": {
17
16
  "type": "string",
@@ -50,8 +49,7 @@
50
49
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
51
50
  },
52
51
  "additionalProperties": {
53
- "type": "string",
54
- "minLength": 1
52
+ "type": "string"
55
53
  }
56
54
  },
57
55
  "tools": {
@@ -61,8 +59,7 @@
61
59
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
62
60
  },
63
61
  "additionalProperties": {
64
- "type": "string",
65
- "minLength": 1
62
+ "type": "string"
66
63
  }
67
64
  },
68
65
  "providers": {
@@ -72,15 +69,15 @@
72
69
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
73
70
  },
74
71
  "additionalProperties": {
75
- "type": "string",
76
- "minLength": 1
72
+ "type": "string"
77
73
  }
78
74
  }
79
75
  },
80
76
  "additionalProperties": {}
81
77
  },
82
78
  "author": {
83
- "type": "string"
79
+ "type": "string",
80
+ "minLength": 1
84
81
  },
85
82
  "providersConfiguration": {
86
83
  "type": "object",
@@ -162,8 +159,9 @@
162
159
  "type": "boolean"
163
160
  },
164
161
  "maxFiles": {
165
- "type": "number",
166
- "exclusiveMinimum": 0
162
+ "type": "integer",
163
+ "exclusiveMinimum": 0,
164
+ "maximum": 9007199254740991
167
165
  }
168
166
  },
169
167
  "required": [
@@ -251,8 +249,9 @@
251
249
  "type": "boolean"
252
250
  },
253
251
  "maxFiles": {
254
- "type": "number",
255
- "exclusiveMinimum": 0
252
+ "type": "integer",
253
+ "exclusiveMinimum": 0,
254
+ "maximum": 9007199254740991
256
255
  }
257
256
  },
258
257
  "required": [
@@ -340,8 +339,9 @@
340
339
  "type": "boolean"
341
340
  },
342
341
  "maxFiles": {
343
- "type": "number",
344
- "exclusiveMinimum": 0
342
+ "type": "integer",
343
+ "exclusiveMinimum": 0,
344
+ "maximum": 9007199254740991
345
345
  }
346
346
  },
347
347
  "required": [
@@ -10,8 +10,7 @@
10
10
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
11
11
  },
12
12
  "version": {
13
- "type": "string",
14
- "minLength": 1
13
+ "type": "string"
15
14
  },
16
15
  "type": {
17
16
  "type": "string",
@@ -49,8 +48,7 @@
49
48
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
50
49
  },
51
50
  "additionalProperties": {
52
- "type": "string",
53
- "minLength": 1
51
+ "type": "string"
54
52
  }
55
53
  },
56
54
  "tools": {
@@ -60,8 +58,7 @@
60
58
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
61
59
  },
62
60
  "additionalProperties": {
63
- "type": "string",
64
- "minLength": 1
61
+ "type": "string"
65
62
  }
66
63
  },
67
64
  "providers": {
@@ -71,8 +68,7 @@
71
68
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
72
69
  },
73
70
  "additionalProperties": {
74
- "type": "string",
75
- "minLength": 1
71
+ "type": "string"
76
72
  }
77
73
  }
78
74
  },
@@ -10,8 +10,7 @@
10
10
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
11
11
  },
12
12
  "version": {
13
- "type": "string",
14
- "minLength": 1
13
+ "type": "string"
15
14
  },
16
15
  "type": {
17
16
  "type": "string",
@@ -49,8 +48,7 @@
49
48
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
50
49
  },
51
50
  "additionalProperties": {
52
- "type": "string",
53
- "minLength": 1
51
+ "type": "string"
54
52
  }
55
53
  },
56
54
  "tools": {
@@ -60,8 +58,7 @@
60
58
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
61
59
  },
62
60
  "additionalProperties": {
63
- "type": "string",
64
- "minLength": 1
61
+ "type": "string"
65
62
  }
66
63
  },
67
64
  "providers": {
@@ -71,8 +68,7 @@
71
68
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
72
69
  },
73
70
  "additionalProperties": {
74
- "type": "string",
75
- "minLength": 1
71
+ "type": "string"
76
72
  }
77
73
  }
78
74
  },
@@ -10,8 +10,7 @@
10
10
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
11
11
  },
12
12
  "version": {
13
- "type": "string",
14
- "minLength": 1
13
+ "type": "string"
15
14
  },
16
15
  "type": {
17
16
  "type": "string",
@@ -49,8 +48,7 @@
49
48
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
50
49
  },
51
50
  "additionalProperties": {
52
- "type": "string",
53
- "minLength": 1
51
+ "type": "string"
54
52
  }
55
53
  },
56
54
  "tools": {
@@ -60,8 +58,7 @@
60
58
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
61
59
  },
62
60
  "additionalProperties": {
63
- "type": "string",
64
- "minLength": 1
61
+ "type": "string"
65
62
  }
66
63
  },
67
64
  "providers": {
@@ -71,8 +68,7 @@
71
68
  "pattern": "^@[a-z0-9]([a-z0-9-]*[a-z0-9])?\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
72
69
  },
73
70
  "additionalProperties": {
74
- "type": "string",
75
- "minLength": 1
71
+ "type": "string"
76
72
  }
77
73
  }
78
74
  },