@conform-ed/contracts 0.0.6 → 0.0.9

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.
@@ -0,0 +1,109 @@
1
+ export {
2
+ H5pMachineNameSchema,
3
+ H5pLibraryFolderNameSchema,
4
+ H5pVersionRefSchema,
5
+ H5pLicenseSchema,
6
+ H5pAuthorSchema,
7
+ H5pChangelogEntrySchema,
8
+ } from "./shared";
9
+ export type {
10
+ H5pMachineName,
11
+ H5pLibraryFolderName,
12
+ H5pVersionRef,
13
+ H5pLicense,
14
+ H5pAuthor,
15
+ H5pChangelogEntry,
16
+ } from "./shared";
17
+
18
+ export { H5pPackageManifestSchema } from "./h5p-json";
19
+ export type { H5pPackageManifest } from "./h5p-json";
20
+
21
+ export { H5pLibraryManifestSchema } from "./library-json";
22
+ export type { H5pLibraryManifest } from "./library-json";
23
+
24
+ export { H5pSemanticsFieldSchema, H5pSemanticsSchema } from "./semantics";
25
+ export type {
26
+ H5pFieldBase,
27
+ H5pTextField,
28
+ H5pHtmlField,
29
+ H5pNumberField,
30
+ H5pBooleanField,
31
+ H5pImageField,
32
+ H5pAudioField,
33
+ H5pVideoField,
34
+ H5pFileField,
35
+ H5pSelectField,
36
+ H5pLibraryField,
37
+ H5pGroupField,
38
+ H5pListField,
39
+ H5pTableField,
40
+ H5pSemanticsField,
41
+ H5pSemantics,
42
+ } from "./semantics";
43
+
44
+ export { H5pCopyrightSchema, H5pMediaFileSchema, H5pLibraryEmbedSchema, H5pContentParamsSchema } from "./content";
45
+ export type { H5pCopyright, H5pMediaFile, H5pLibraryEmbed, H5pContentParams } from "./content";
46
+
47
+ import { H5pPackageManifestSchema } from "./h5p-json";
48
+ import { H5pLibraryManifestSchema } from "./library-json";
49
+ import { H5pSemanticsFieldSchema, H5pSemanticsSchema } from "./semantics";
50
+ import { H5pCopyrightSchema, H5pMediaFileSchema, H5pLibraryEmbedSchema, H5pContentParamsSchema } from "./content";
51
+ import {
52
+ H5pMachineNameSchema,
53
+ H5pLibraryFolderNameSchema,
54
+ H5pVersionRefSchema,
55
+ H5pLicenseSchema,
56
+ H5pAuthorSchema,
57
+ H5pChangelogEntrySchema,
58
+ } from "./shared";
59
+
60
+ export namespace H5pV1 {
61
+ export namespace Schemas {
62
+ export const PackageManifest = H5pPackageManifestSchema;
63
+ export const LibraryManifest = H5pLibraryManifestSchema;
64
+ export const SemanticsField = H5pSemanticsFieldSchema;
65
+ export const Semantics = H5pSemanticsSchema;
66
+ export const Copyright = H5pCopyrightSchema;
67
+ export const MediaFile = H5pMediaFileSchema;
68
+ export const LibraryEmbed = H5pLibraryEmbedSchema;
69
+ export const ContentParams = H5pContentParamsSchema;
70
+ }
71
+
72
+ export namespace Shared {
73
+ export const MachineName = H5pMachineNameSchema;
74
+ export const LibraryFolderName = H5pLibraryFolderNameSchema;
75
+ export const VersionRef = H5pVersionRefSchema;
76
+ export const License = H5pLicenseSchema;
77
+ export const Author = H5pAuthorSchema;
78
+ export const ChangelogEntry = H5pChangelogEntrySchema;
79
+ }
80
+ }
81
+
82
+ export type H5pV1Schemas = typeof H5pV1.Schemas;
83
+
84
+ export const H5pV1DerivedZodTemplates = {
85
+ description: "H5P package and library Zod schemas for h5p.json, library.json, semantics.json, and content.json",
86
+ specLinks: {
87
+ specification: "https://h5p.org/documentation/developers/h5p-specification",
88
+ jsonFiles: "https://h5p.org/documentation/developers/json-file-descriptions",
89
+ semantics: "https://h5p.org/documentation/developers/semantics",
90
+ phpLibrary: "https://github.com/h5p/h5p-php-library",
91
+ },
92
+ scope:
93
+ "h5p.json (PackageManifest), library.json (LibraryManifest), semantics.json (SemanticsField array), content.json base types",
94
+ coreSchemas: ["H5pPackageManifestSchema", "H5pLibraryManifestSchema", "H5pSemanticsSchema"],
95
+ notes: [
96
+ "content.json structure varies per library — H5pContentParamsSchema is a permissive base (z.record). Full content validation requires semantics traversal.",
97
+ "Patch version intentionally excluded from H5pVersionRefSchema — any patch of the specified major.minor is acceptable per spec.",
98
+ "runnable and fullscreen are 0|1 integers, not booleans, matching the H5P spec.",
99
+ "PHP reference library (h5p-php-library) is GPL-3.0; NodeJS reference (Lumieducation/h5p-nodejs-library) is a full server framework. Neither is taken as a dependency.",
100
+ "Validation regex patterns for machineName and library folder names are derived from H5PValidator in h5p-php-library.",
101
+ ],
102
+ } as const;
103
+
104
+ export const DerivedZodTemplates = H5pV1DerivedZodTemplates;
105
+
106
+ // Convenience aliases so that `H5pV1.Schemas.*` and `H5pV1.Shared.*` work
107
+ // when consumers import via `export * as H5pV1 from "./h5p/v1"`.
108
+ export const Schemas = H5pV1.Schemas;
109
+ export const Shared = H5pV1.Shared;
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { strictObject, H5pMachineNameSchema, H5pVersionRefSchema } from "./shared";
3
+
4
+ const filePathSchema = strictObject({ path: z.string().min(1) });
5
+
6
+ const runnableFlag = z.union([z.literal(0), z.literal(1)]);
7
+
8
+ // library.json — required in every H5P library directory.
9
+ // Controls loading, dependencies, embedding, and editor capabilities.
10
+ export const H5pLibraryManifestSchema = strictObject({
11
+ // Required fields
12
+ title: z.string().min(1),
13
+ machineName: H5pMachineNameSchema,
14
+ majorVersion: z.number().int().nonnegative().max(99999),
15
+ minorVersion: z.number().int().nonnegative().max(99999),
16
+ patchVersion: z.number().int().nonnegative().max(99999),
17
+ // 1 = standalone runnable content type, 0 = helper library only
18
+ runnable: runnableFlag,
19
+
20
+ // Optional fields
21
+ description: z.string().optional(),
22
+ author: z.string().optional(),
23
+ // library.json accepts any SPDX license string (e.g. "MIT", "Apache-2.0").
24
+ // Unlike h5p.json which restricts to H5P's enumerated content licenses.
25
+ license: z.string().max(32).optional(),
26
+
27
+ // JavaScript and CSS assets to preload
28
+ preloadedJs: z.array(filePathSchema).optional(),
29
+ preloadedCss: z.array(filePathSchema).optional(),
30
+
31
+ // Runtime dependencies (patch version excluded per spec)
32
+ preloadedDependencies: z.array(H5pVersionRefSchema).optional(),
33
+ // Loaded on-demand during execution
34
+ dynamicDependencies: z.array(H5pVersionRefSchema).optional(),
35
+ // Required only when the H5P editor is in use
36
+ editorDependencies: z.array(H5pVersionRefSchema).optional(),
37
+
38
+ // Embedding dimensions — required for iframe embed type
39
+ w: z.number().int().positive().optional(),
40
+ h: z.number().int().positive().optional(),
41
+ embedTypes: z.array(z.enum(["iframe", "div"])).optional(),
42
+ // 1 = supports fullscreen mode
43
+ fullscreen: runnableFlag.optional(),
44
+
45
+ contentType: z.string().optional(),
46
+
47
+ // Minimum H5P core API version this library requires
48
+ coreApi: strictObject({
49
+ majorVersion: z.number().int().nonnegative(),
50
+ minorVersion: z.number().int().nonnegative(),
51
+ }).optional(),
52
+
53
+ metadataSettings: strictObject({
54
+ disable: runnableFlag.optional(),
55
+ disableExtraTitleField: runnableFlag.optional(),
56
+ }).optional(),
57
+ }).superRefine((data, ctx) => {
58
+ // iframe embedding requires explicit dimensions
59
+ if (data.runnable === 1 && data.embedTypes?.includes("iframe")) {
60
+ if (data.w === undefined || data.h === undefined) {
61
+ ctx.addIssue({
62
+ code: z.ZodIssueCode.custom,
63
+ path: ["w"],
64
+ message: 'Runnable libraries with embedType "iframe" should declare w and h dimensions',
65
+ });
66
+ }
67
+ }
68
+ });
69
+
70
+ export type H5pLibraryManifest = z.infer<typeof H5pLibraryManifestSchema>;
@@ -0,0 +1,268 @@
1
+ import { z } from "zod";
2
+
3
+ // Conditional visibility rule for a field — renders/hides based on another field's value.
4
+ const showWhenRuleSchema = z.object({
5
+ field: z.string(),
6
+ equals: z.union([z.string(), z.boolean(), z.number(), z.array(z.string())]),
7
+ });
8
+
9
+ const showWhenSchema = z.object({
10
+ type: z.enum(["and", "or"]).optional(),
11
+ rules: z.array(showWhenRuleSchema),
12
+ });
13
+
14
+ // Shared optional base properties present on all field types.
15
+ const baseFieldShape = {
16
+ name: z.string().min(1),
17
+ label: z.string().optional(),
18
+ description: z.string().optional(),
19
+ importance: z.enum(["high", "medium", "low"]).optional(),
20
+ optional: z.boolean().optional(),
21
+ common: z.boolean().optional(),
22
+ default: z.unknown().optional(),
23
+ widget: z.string().optional(),
24
+ showWhen: showWhenSchema.optional(),
25
+ // When field is hidden by showWhen, reset value to null
26
+ nullWhenHidden: z.boolean().optional(),
27
+ // Override default copy/paste behaviour in editor
28
+ copy: z.boolean().optional(),
29
+ };
30
+
31
+ // TypeScript-side recursive types must be declared manually to avoid circular inference.
32
+ export interface H5pFieldBase {
33
+ name: string;
34
+ label?: string;
35
+ description?: string;
36
+ importance?: "high" | "medium" | "low";
37
+ optional?: boolean;
38
+ common?: boolean;
39
+ default?: unknown;
40
+ widget?: string;
41
+ showWhen?: { type?: "and" | "or"; rules: Array<{ field: string; equals: unknown }> };
42
+ nullWhenHidden?: boolean;
43
+ copy?: boolean;
44
+ }
45
+
46
+ export interface H5pTextField extends H5pFieldBase {
47
+ type: "text";
48
+ maxLength?: number;
49
+ minLength?: number;
50
+ regexp?: { pattern: string; modifiers?: string; description?: string };
51
+ placeholder?: string;
52
+ enterMode?: string;
53
+ tags?: string[];
54
+ font?: unknown;
55
+ }
56
+ export interface H5pHtmlField extends H5pFieldBase {
57
+ type: "html";
58
+ tags?: string[];
59
+ maxLength?: number;
60
+ minLength?: number;
61
+ placeholder?: string;
62
+ font?: unknown;
63
+ }
64
+ export interface H5pNumberField extends H5pFieldBase {
65
+ type: "number";
66
+ min?: number;
67
+ max?: number;
68
+ steps?: number;
69
+ decimals?: number;
70
+ }
71
+ export interface H5pBooleanField extends H5pFieldBase {
72
+ type: "boolean";
73
+ }
74
+ export interface H5pImageField extends H5pFieldBase {
75
+ type: "image";
76
+ allowedMimeTypes?: string[];
77
+ disableCopyright?: boolean;
78
+ }
79
+ export interface H5pAudioField extends H5pFieldBase {
80
+ type: "audio";
81
+ allowedMimeTypes?: string[];
82
+ disableConversion?: boolean;
83
+ }
84
+ export interface H5pVideoField extends H5pFieldBase {
85
+ type: "video";
86
+ allowedMimeTypes?: string[];
87
+ disableConversion?: boolean;
88
+ }
89
+ export interface H5pFileField extends H5pFieldBase {
90
+ type: "file";
91
+ allowedMimeTypes?: string[];
92
+ }
93
+ export interface H5pSelectField extends H5pFieldBase {
94
+ type: "select";
95
+ options: Array<{ value: string; label: string }>;
96
+ multiple?: boolean;
97
+ }
98
+ export interface H5pLibraryField extends H5pFieldBase {
99
+ type: "library";
100
+ options?: string[];
101
+ }
102
+ export interface H5pGroupField extends H5pFieldBase {
103
+ type: "group";
104
+ fields: H5pSemanticsField[];
105
+ isSubContent?: boolean;
106
+ expanded?: boolean;
107
+ }
108
+ export interface H5pListField extends H5pFieldBase {
109
+ type: "list";
110
+ field: H5pSemanticsField;
111
+ min?: number;
112
+ max?: number;
113
+ entity?: string;
114
+ widgets?: Array<{ name: string; label?: string }>;
115
+ }
116
+ export interface H5pTableField extends H5pFieldBase {
117
+ type: "table";
118
+ columns: H5pSemanticsField[];
119
+ rows?: number;
120
+ }
121
+
122
+ export type H5pSemanticsField =
123
+ | H5pTextField
124
+ | H5pHtmlField
125
+ | H5pNumberField
126
+ | H5pBooleanField
127
+ | H5pImageField
128
+ | H5pAudioField
129
+ | H5pVideoField
130
+ | H5pFileField
131
+ | H5pSelectField
132
+ | H5pLibraryField
133
+ | H5pGroupField
134
+ | H5pListField
135
+ | H5pTableField;
136
+
137
+ // --- Zod schemas ---
138
+
139
+ // Leaf field schemas (no recursion)
140
+ const textFieldSchema = z.object({
141
+ ...baseFieldShape,
142
+ type: z.literal("text"),
143
+ maxLength: z.number().int().positive().optional(),
144
+ minLength: z.number().int().nonnegative().optional(),
145
+ regexp: z
146
+ .object({ pattern: z.string(), modifiers: z.string().optional(), description: z.string().optional() })
147
+ .optional(),
148
+ placeholder: z.string().optional(),
149
+ enterMode: z.string().optional(),
150
+ tags: z.array(z.string()).optional(),
151
+ font: z.unknown().optional(),
152
+ });
153
+
154
+ const htmlFieldSchema = z.object({
155
+ ...baseFieldShape,
156
+ type: z.literal("html"),
157
+ tags: z.array(z.string()).optional(),
158
+ maxLength: z.number().int().positive().optional(),
159
+ minLength: z.number().int().nonnegative().optional(),
160
+ placeholder: z.string().optional(),
161
+ font: z.unknown().optional(),
162
+ });
163
+
164
+ const numberFieldSchema = z.object({
165
+ ...baseFieldShape,
166
+ type: z.literal("number"),
167
+ min: z.number().optional(),
168
+ max: z.number().optional(),
169
+ steps: z.number().positive().optional(),
170
+ decimals: z.number().int().nonnegative().optional(),
171
+ });
172
+
173
+ const booleanFieldSchema = z.object({
174
+ ...baseFieldShape,
175
+ type: z.literal("boolean"),
176
+ });
177
+
178
+ const imageFieldSchema = z.object({
179
+ ...baseFieldShape,
180
+ type: z.literal("image"),
181
+ allowedMimeTypes: z.array(z.string()).optional(),
182
+ disableCopyright: z.boolean().optional(),
183
+ });
184
+
185
+ const audioFieldSchema = z.object({
186
+ ...baseFieldShape,
187
+ type: z.literal("audio"),
188
+ allowedMimeTypes: z.array(z.string()).optional(),
189
+ disableConversion: z.boolean().optional(),
190
+ });
191
+
192
+ const videoFieldSchema = z.object({
193
+ ...baseFieldShape,
194
+ type: z.literal("video"),
195
+ allowedMimeTypes: z.array(z.string()).optional(),
196
+ disableConversion: z.boolean().optional(),
197
+ });
198
+
199
+ const fileFieldSchema = z.object({
200
+ ...baseFieldShape,
201
+ type: z.literal("file"),
202
+ allowedMimeTypes: z.array(z.string()).optional(),
203
+ });
204
+
205
+ const selectFieldSchema = z.object({
206
+ ...baseFieldShape,
207
+ type: z.literal("select"),
208
+ options: z.array(z.object({ value: z.string(), label: z.string() })),
209
+ multiple: z.boolean().optional(),
210
+ });
211
+
212
+ const libraryFieldSchema = z.object({
213
+ ...baseFieldShape,
214
+ type: z.literal("library"),
215
+ options: z.array(z.string()).optional(),
216
+ });
217
+
218
+ // Recursive field schemas — use z.lazy so they can reference the outer union.
219
+ // Using let-then-assign pattern (same as cmi5 Block/Au schemas).
220
+ let h5pSemanticsFieldSchemaInternal: z.ZodType<H5pSemanticsField>;
221
+
222
+ const groupFieldSchema: z.ZodType<H5pGroupField> = z.object({
223
+ ...baseFieldShape,
224
+ type: z.literal("group"),
225
+ fields: z.lazy(() => z.array(h5pSemanticsFieldSchemaInternal)),
226
+ isSubContent: z.boolean().optional(),
227
+ expanded: z.boolean().optional(),
228
+ }) as z.ZodType<H5pGroupField>;
229
+
230
+ const listFieldSchema: z.ZodType<H5pListField> = z.object({
231
+ ...baseFieldShape,
232
+ type: z.literal("list"),
233
+ field: z.lazy(() => h5pSemanticsFieldSchemaInternal),
234
+ min: z.number().int().nonnegative().optional(),
235
+ max: z.number().int().positive().optional(),
236
+ entity: z.string().optional(),
237
+ widgets: z.array(z.object({ name: z.string(), label: z.string().optional() })).optional(),
238
+ }) as z.ZodType<H5pListField>;
239
+
240
+ const tableFieldSchema: z.ZodType<H5pTableField> = z.object({
241
+ ...baseFieldShape,
242
+ type: z.literal("table"),
243
+ columns: z.lazy(() => z.array(h5pSemanticsFieldSchemaInternal)),
244
+ rows: z.number().int().positive().optional(),
245
+ }) as z.ZodType<H5pTableField>;
246
+
247
+ h5pSemanticsFieldSchemaInternal = z.union([
248
+ textFieldSchema,
249
+ htmlFieldSchema,
250
+ numberFieldSchema,
251
+ booleanFieldSchema,
252
+ imageFieldSchema,
253
+ audioFieldSchema,
254
+ videoFieldSchema,
255
+ fileFieldSchema,
256
+ selectFieldSchema,
257
+ libraryFieldSchema,
258
+ groupFieldSchema,
259
+ listFieldSchema,
260
+ tableFieldSchema,
261
+ ]) as z.ZodType<H5pSemanticsField>;
262
+
263
+ // The schema for a single semantics field (any type).
264
+ export const H5pSemanticsFieldSchema: z.ZodType<H5pSemanticsField> = h5pSemanticsFieldSchemaInternal;
265
+
266
+ // The top-level semantics.json document — an array of field definitions.
267
+ export const H5pSemanticsSchema = z.array(H5pSemanticsFieldSchema);
268
+ export type H5pSemantics = z.infer<typeof H5pSemanticsSchema>;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+
3
+ export function strictObject<T extends z.ZodRawShape>(shape: T) {
4
+ return z.object(shape).strict();
5
+ }
6
+
7
+ // Machine name: alphanumeric, hyphens, dots — used in machineName fields and folder names.
8
+ // Pattern from h5p-php-library H5PValidator::isValidRequiredH5pData
9
+ export const H5pMachineNameSchema = z.string().regex(/^[\w0-9-.]{1,255}$/iu);
10
+
11
+ // Library folder name without patch: "H5P.Image-1.1", "H5P.Column-1.22"
12
+ // Patch version is intentionally excluded from folder names and dependency references per spec.
13
+ export const H5pLibraryFolderNameSchema = z.string().regex(/^[\w0-9-.]{1,255}-\d{1,5}\.\d{1,5}$/u);
14
+
15
+ // Version reference used in preloadedDependencies / dynamicDependencies / editorDependencies.
16
+ // Patch is intentionally omitted — any patch of the specified major.minor is acceptable.
17
+ export const H5pVersionRefSchema = strictObject({
18
+ machineName: H5pMachineNameSchema,
19
+ majorVersion: z.number().int().nonnegative().max(99999),
20
+ minorVersion: z.number().int().nonnegative().max(99999),
21
+ });
22
+
23
+ // Standard H5P license codes as used in h5p.json and library.json.
24
+ // "U" = undisclosed/unknown (H5P default when no license is specified).
25
+ export const H5pLicenseSchema = z.enum([
26
+ "CC BY",
27
+ "CC BY-SA",
28
+ "CC BY-NC",
29
+ "CC BY-NC-SA",
30
+ "CC BY-ND",
31
+ "CC BY-NC-ND",
32
+ "CC0",
33
+ "GNU GPL",
34
+ "PD",
35
+ "ODC PDDL",
36
+ "CC PDM",
37
+ "U",
38
+ ]);
39
+
40
+ export const H5pAuthorSchema = strictObject({
41
+ name: z.string().min(1),
42
+ role: z.string().optional(),
43
+ });
44
+
45
+ export const H5pChangelogEntrySchema = strictObject({
46
+ date: z.string(),
47
+ changes: z.array(z.string()),
48
+ });
49
+
50
+ // Inferred types
51
+ export type H5pMachineName = z.infer<typeof H5pMachineNameSchema>;
52
+ export type H5pLibraryFolderName = z.infer<typeof H5pLibraryFolderNameSchema>;
53
+ export type H5pVersionRef = z.infer<typeof H5pVersionRefSchema>;
54
+ export type H5pLicense = z.infer<typeof H5pLicenseSchema>;
55
+ export type H5pAuthor = z.infer<typeof H5pAuthorSchema>;
56
+ export type H5pChangelogEntry = z.infer<typeof H5pChangelogEntrySchema>;
package/src/index.ts CHANGED
@@ -30,3 +30,5 @@ export * as ClrV2_0 from "./clr/v2_0";
30
30
  export * as OpenBadgesV3_0 from "./open-badges/v3_0";
31
31
  export * as OneRosterV1_2 from "./oneroster/v1_2";
32
32
  export * as VcDataModelV2_0 from "./vc-data-model/v2_0";
33
+ export * as H5pV1 from "./h5p/v1";
34
+ export { H5pV1DerivedZodTemplates } from "./h5p/v1";
@@ -29,7 +29,7 @@ export const LineItemSchema = strictObject({
29
29
  gradesReleased: z.boolean().optional(),
30
30
  });
31
31
 
32
- export const ScoreProgressSchema = z.enum(["Completed", "Failed", "InProgress", "Initialized", "Submitted", "Started"]);
32
+ export const ScoreProgressSchema = z.enum(["Completed", "InProgress", "Initialized", "Submitted", "Started"]);
33
33
  export const GradingProgressSchema = z.enum(["Failed", "FullyGraded", "NotReady", "Pending", "PendingManual"]);
34
34
 
35
35
  export const ScoreSubmissionSchema = strictObject({
@@ -53,7 +53,8 @@ export const AgentSchema = strictObject({
53
53
  if (ifiCount !== 1) {
54
54
  ctx.addIssue({
55
55
  code: z.ZodIssueCode.custom,
56
- message: "An xAPI Agent requires exactly one Inverse Functional Identifier (mbox, mbox_sha1sum, openid, or account)",
56
+ message:
57
+ "An xAPI Agent requires exactly one Inverse Functional Identifier (mbox, mbox_sha1sum, openid, or account)",
57
58
  });
58
59
  }
59
60
  });
@@ -241,6 +242,7 @@ export const AttachmentSchema = strictObject({
241
242
  length: z.number().int().nonnegative(),
242
243
  sha2: AttachmentSha2Schema,
243
244
  fileUrl: z.url().optional(),
245
+ contentBase64: z.string().optional(),
244
246
  });
245
247
 
246
248
  export const XapiDocumentSchema = strictObject({