@conform-ed/contracts 0.0.3
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +196 -0
- package/.turbo/turbo-typecheck.log +2 -0
- package/README.md +52 -0
- package/caliper-v1_2-zod-templates.md +123 -0
- package/case-v1_1-zod-templates.md +290 -0
- package/cat-v1_0-zod-templates.md +73 -0
- package/cc-v1_3-zod-templates.md +531 -0
- package/cc-v1_4-zod-templates.md +247 -0
- package/clr-v2_0-zod-templates.md +117 -0
- package/cmi5-v1_0-zod-templates.md +9 -0
- package/lti-zod-templates.md +11 -0
- package/oneroster-v1_2-zod-templates.md +76 -0
- package/open-badges-v3_0-zod-templates.md +66 -0
- package/package.json +42 -0
- package/qti-v2_1-zod-templates.md +45 -0
- package/qti-v2_2-zod-templates.md +32 -0
- package/qti-v3_0_1-zod-templates.md +421 -0
- package/src/adapter.ts +83 -0
- package/src/caliper/v1_2/caliper_v1p2_bootcamp_schema.ts +781 -0
- package/src/caliper/v1_2/index.ts +37 -0
- package/src/caliper/v1_2/shared.ts +122 -0
- package/src/caliper/v1_2/textual_requirements.ts +219 -0
- package/src/case/v1_1/case_v1p1_cfassociation_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfassociationgrouping_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfassociationset_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfconceptset_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfdocument_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfdocumentset_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfitem_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfitemtypeset_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cflicense_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfpackage_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfrubric_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_cfsubjectset_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_imsx_statusinfo_jsonschema1.ts +1 -0
- package/src/case/v1_1/case_v1p1_openapi3_restbinding_schema.ts +113 -0
- package/src/case/v1_1/index.ts +95 -0
- package/src/case/v1_1/shared.ts +384 -0
- package/src/cat/v1_0/cat_v1p0_restbinding_operations_schema.ts +75 -0
- package/src/cat/v1_0/index.ts +124 -0
- package/src/cat/v1_0/shared.ts +345 -0
- package/src/clr/v2_0/clr_v2p0_achievementcredential_schema.ts +16 -0
- package/src/clr/v2_0/clr_v2p0_clrcredential_schema.ts +9 -0
- package/src/clr/v2_0/clr_v2p0_endorsementcredential_schema.ts +11 -0
- package/src/clr/v2_0/clr_v2p0_getclrcredentialsresponse_schema.ts +1 -0
- package/src/clr/v2_0/clr_v2p0_imsx_statusinfo_schema.ts +8 -0
- package/src/clr/v2_0/clr_v2p0_profile_schema.ts +9 -0
- package/src/clr/v2_0/index.ts +22 -0
- package/src/clr/v2_0/shared.ts +206 -0
- package/src/cmi5/index.ts +3 -0
- package/src/cmi5/v1_0/index.ts +134 -0
- package/src/common-cartridge/v1_3/ccv1p3_imsccauth_v1p3.ts +26 -0
- package/src/common-cartridge/v1_3/ccv1p3_imscp_v1p2_v1p0.ts +352 -0
- package/src/common-cartridge/v1_3/ccv1p3_imscsmd_v1p0.ts +35 -0
- package/src/common-cartridge/v1_3/ccv1p3_imsdt_v1p3.ts +33 -0
- package/src/common-cartridge/v1_3/ccv1p3_imswl_v1p3.ts +23 -0
- package/src/common-cartridge/v1_3/ccv1p3_lomccltilink_v1p0.ts +14 -0
- package/src/common-cartridge/v1_3/ccv1p3_lommanifest_v1p0.ts +14 -0
- package/src/common-cartridge/v1_3/ccv1p3_lomresource_v1p0.ts +14 -0
- package/src/common-cartridge/v1_3/ccv1p3_qtiasiv1p2p1_v1p0.ts +964 -0
- package/src/common-cartridge/v1_3/index.ts +41 -0
- package/src/common-cartridge/v1_3/lom-internal.ts +396 -0
- package/src/common-cartridge/v1_3/shared.ts +68 -0
- package/src/common-cartridge/v1_4/core/ccv1p4_imscp_v1p2_v1p0.ts +360 -0
- package/src/common-cartridge/v1_4/core/ccv1p4_lommanifest_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/core/ccv1p4_lomresource_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/extension/cc_extresource_assignmentv1p0_v1p0.ts +61 -0
- package/src/common-cartridge/v1_4/extension/ccv1p4_cpextensionv1p2_v1p0.ts +20 -0
- package/src/common-cartridge/v1_4/extension/ims_openvideov1p0_v1p0.ts +325 -0
- package/src/common-cartridge/v1_4/index.ts +104 -0
- package/src/common-cartridge/v1_4/k12/ccv1p4_imscp_v1p2_v1p0.ts +19 -0
- package/src/common-cartridge/v1_4/k12/ccv1p4_imscp_v1p2_v1p0_thin.ts +17 -0
- package/src/common-cartridge/v1_4/k12/ccv1p4_lommanifest_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/k12/ccv1p4_lomresource_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/lom-internal.ts +476 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_imsccauth_v1p4.ts +26 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_imscsmd_v1p1.ts +36 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_imsdt_v1p4.ts +33 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_imslticc_v1p4.ts +45 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_imswl_v1p4.ts +23 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_lomccltilink_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/shared/ccv1p4_qtiasiv1p2p1_v1p0.ts +964 -0
- package/src/common-cartridge/v1_4/shared/imsbasiclti_v1p0p1.ts +24 -0
- package/src/common-cartridge/v1_4/shared/imslticm_v1p0.ts +23 -0
- package/src/common-cartridge/v1_4/shared/imslticp_v1p0.ts +57 -0
- package/src/common-cartridge/v1_4/shared/lineitem_v1p0.ts +35 -0
- package/src/common-cartridge/v1_4/shared/resourcea11ymetadata-20210915.ts +110 -0
- package/src/common-cartridge/v1_4/shared.ts +68 -0
- package/src/common-cartridge/v1_4/thin/ccv1p4_imscp_v1p2_v1p0.ts +243 -0
- package/src/common-cartridge/v1_4/thin/ccv1p4_lommanifest_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/thin/ccv1p4_lomresource_v1p0.ts +14 -0
- package/src/common-cartridge/v1_4/vdex/imsmd_loose_v1p3p2.ts +13 -0
- package/src/common-cartridge/v1_4/vdex/imsvdex_v1p0.ts +124 -0
- package/src/config.ts +121 -0
- package/src/index.ts +32 -0
- package/src/lti/ags/v2_0/index.ts +97 -0
- package/src/lti/deep-linking/v2_0/index.ts +100 -0
- package/src/lti/index.ts +26 -0
- package/src/lti/nrps/v2_0/index.ts +69 -0
- package/src/lti/proctoring/v1_0/index.ts +131 -0
- package/src/lti/shared.ts +66 -0
- package/src/lti/v1_3/index.ts +84 -0
- package/src/oneroster/v1_2/index.ts +156 -0
- package/src/oneroster/v1_2/or_v1p2_csv_binding_schema.ts +427 -0
- package/src/oneroster/v1_2/or_v1p2_gradebook_restbinding_schema.ts +120 -0
- package/src/oneroster/v1_2/or_v1p2_gradebook_service_schema.ts +243 -0
- package/src/oneroster/v1_2/or_v1p2_resource_restbinding_schema.ts +24 -0
- package/src/oneroster/v1_2/or_v1p2_resource_service_schema.ts +47 -0
- package/src/oneroster/v1_2/or_v1p2_rostering_restbinding_schema.ts +84 -0
- package/src/oneroster/v1_2/or_v1p2_rostering_service_schema.ts +288 -0
- package/src/oneroster/v1_2/shared.ts +90 -0
- package/src/open-badges/v3_0/index.ts +20 -0
- package/src/open-badges/v3_0/ob_v3p0_achievementcredential_schema.ts +17 -0
- package/src/open-badges/v3_0/ob_v3p0_endorsementcredential_schema.ts +11 -0
- package/src/open-badges/v3_0/ob_v3p0_getopenbadgecredentialsresponse_schema.ts +1 -0
- package/src/open-badges/v3_0/ob_v3p0_imsx_statusinfo_schema.ts +8 -0
- package/src/open-badges/v3_0/ob_v3p0_profile_schema.ts +9 -0
- package/src/open-badges/v3_0/shared.ts +458 -0
- package/src/qti/v2-internal.ts +1683 -0
- package/src/qti/v2_1/apipv1p0_qtiextv2p1_v1p0.ts +3 -0
- package/src/qti/v2_1/imsqti_metadata_v2p1.ts +3 -0
- package/src/qti/v2_1/imsqti_result_v2p1.ts +3 -0
- package/src/qti/v2_1/imsqti_usagedata_v2p1.ts +3 -0
- package/src/qti/v2_1/imsqti_v2p1p2.ts +71 -0
- package/src/qti/v2_1/index.ts +53 -0
- package/src/qti/v2_1/qtiv2p1_imscpv1p2_v1p0.ts +3 -0
- package/src/qti/v2_1/schemas.ts +15 -0
- package/src/qti/v2_1/shared.ts +3 -0
- package/src/qti/v2_2/apipv1p0_qtiextv2p2_v1p0p1.ts +3 -0
- package/src/qti/v2_2/imsqti_metadata_v2p2.ts +3 -0
- package/src/qti/v2_2/imsqti_result_v2p2.ts +3 -0
- package/src/qti/v2_2/imsqti_usagedata_v2p2.ts +3 -0
- package/src/qti/v2_2/imsqti_v2p2p4.ts +76 -0
- package/src/qti/v2_2/imsqtiv2p2p4_html5_v1p0.ts +8 -0
- package/src/qti/v2_2/index.ts +59 -0
- package/src/qti/v2_2/qtiv2p2_csm_v2p2.ts +4 -0
- package/src/qti/v2_2/qtiv2p2_imscpv1p2_v1p0.ts +3 -0
- package/src/qti/v2_2/schemas.ts +19 -0
- package/src/qti/v2_2/shared.ts +3 -0
- package/src/qti/v3_0_1/assessment-internal.ts +1509 -0
- package/src/qti/v3_0_1/imsqti_asiv3p0p1_v1p0.ts +57 -0
- package/src/qti/v3_0_1/imsqti_itemv3p0p1_v1p0.ts +60 -0
- package/src/qti/v3_0_1/imsqti_metadatav3p0_v1p0.ts +73 -0
- package/src/qti/v3_0_1/imsqti_outcomev3p0p1_v1p0.ts +11 -0
- package/src/qti/v3_0_1/imsqti_responseprocessingv3p0p1_v1p0.ts +17 -0
- package/src/qti/v3_0_1/imsqti_resultv3p0_v1p0.ts +222 -0
- package/src/qti/v3_0_1/imsqti_sectionv3p0p1_v1p0.ts +15 -0
- package/src/qti/v3_0_1/imsqti_stimulusv3p0p1_v1p0.ts +15 -0
- package/src/qti/v3_0_1/imsqti_testv3p0p1_v1p0.ts +49 -0
- package/src/qti/v3_0_1/imsqti_usagedatav3p0_v1p0.ts +77 -0
- package/src/qti/v3_0_1/imsqtiv3p0_afa3p0pnp_v1p0.ts +269 -0
- package/src/qti/v3_0_1/index.ts +50 -0
- package/src/qti/v3_0_1/processing-internal.ts +667 -0
- package/src/qti/v3_0_1/shared.ts +146 -0
- package/src/qti/v3_0_1/variables-internal.ts +274 -0
- package/src/shared/imsx-status.ts +49 -0
- package/src/summary.ts +70 -0
- package/src/vc-data-model/v2_0/index.ts +27 -0
- package/src/vc-data-model/v2_0/shared.ts +206 -0
- package/src/xapi/index.ts +12 -0
- package/src/xapi/shared.ts +444 -0
- package/src/xapi/v1_0_3/index.ts +169 -0
- package/src/xapi/v2_0/index.ts +256 -0
- package/test/caliper-v1_2.test.ts +270 -0
- package/test/case-v1_1.test.ts +147 -0
- package/test/cat-v1_0.test.ts +338 -0
- package/test/cc-v1_4.test.ts +239 -0
- package/test/clr-v2_0.test.ts +225 -0
- package/test/cmi5-v1_0.test.ts +196 -0
- package/test/contracts.test.ts +125 -0
- package/test/fixtures/cmi5/extended-cmi5.xml +72 -0
- package/test/fixtures/cmi5/simple-cmi5.xml +26 -0
- package/test/lti.test.ts +146 -0
- package/test/oneroster-v1_2.test.ts +234 -0
- package/test/open-badges-v3_0.test.ts +144 -0
- package/test/qti-v2_1.test.ts +146 -0
- package/test/qti-v2_2.test.ts +154 -0
- package/test/qti-v3_0_1.test.ts +576 -0
- package/test/schema-examples.test.ts +101 -0
- package/test/vc-data-model-v2_0.test.ts +63 -0
- package/test/xapi.test.ts +384 -0
- package/tsconfig.json +7 -0
- package/vc-data-model-v2_0-zod-templates.md +43 -0
- package/xapi-zod-templates.md +95 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { VcDataModelV2_0 } from "../src";
|
|
4
|
+
|
|
5
|
+
test("VerifiableCredentialSchema parses a minimal VC Data Model 2.0 credential", () => {
|
|
6
|
+
const parsed = VcDataModelV2_0.VerifiableCredentialSchema.safeParse({
|
|
7
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
8
|
+
type: "VerifiableCredential",
|
|
9
|
+
issuer: "https://example.test/issuers/1",
|
|
10
|
+
validFrom: "2025-01-01T00:00:00Z",
|
|
11
|
+
credentialSubject: {
|
|
12
|
+
id: "did:example:subject-1",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(parsed.success).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("VerifiablePresentationSchema parses a presentation carrying VC and JWS credentials", () => {
|
|
20
|
+
const parsed = VcDataModelV2_0.VerifiablePresentationSchema.safeParse({
|
|
21
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
22
|
+
type: "VerifiablePresentation",
|
|
23
|
+
holder: "did:example:holder-1",
|
|
24
|
+
verifiableCredential: [
|
|
25
|
+
{
|
|
26
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
27
|
+
type: "VerifiableCredential",
|
|
28
|
+
issuer: "https://example.test/issuers/1",
|
|
29
|
+
validFrom: "2025-01-01T00:00:00Z",
|
|
30
|
+
credentialSubject: {
|
|
31
|
+
id: "did:example:subject-1",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"header.payload.signature",
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(parsed.success).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("VerifiableCredentialSchema rejects wrong core context", () => {
|
|
42
|
+
const parsed = VcDataModelV2_0.VerifiableCredentialSchema.safeParse({
|
|
43
|
+
"@context": ["https://example.test/context"],
|
|
44
|
+
type: "VerifiableCredential",
|
|
45
|
+
issuer: "https://example.test/issuers/1",
|
|
46
|
+
validFrom: "2025-01-01T00:00:00Z",
|
|
47
|
+
credentialSubject: {
|
|
48
|
+
id: "did:example:subject-1",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(parsed.success).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("VcDataModel20DerivedZodTemplates exposes expected entry points", () => {
|
|
56
|
+
expect(VcDataModelV2_0.VcDataModel20DerivedZodTemplates.verifiableCredential).toBe(
|
|
57
|
+
VcDataModelV2_0.VerifiableCredentialSchema,
|
|
58
|
+
);
|
|
59
|
+
expect(VcDataModelV2_0.VcDataModel20DerivedZodTemplates.verifiablePresentation).toBe(
|
|
60
|
+
VcDataModelV2_0.VerifiablePresentationSchema,
|
|
61
|
+
);
|
|
62
|
+
expect(VcDataModelV2_0.VcDataModel20DerivedZodTemplates.proof).toBe(VcDataModelV2_0.ProofSchema);
|
|
63
|
+
});
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { XapiV1_0_3, XapiV2_0 } from "@conform-ed/contracts";
|
|
3
|
+
|
|
4
|
+
const attachmentSha2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
5
|
+
|
|
6
|
+
const validStatement = {
|
|
7
|
+
actor: {
|
|
8
|
+
objectType: "Agent",
|
|
9
|
+
mbox: "mailto:alice@example.com",
|
|
10
|
+
name: "Alice",
|
|
11
|
+
},
|
|
12
|
+
verb: {
|
|
13
|
+
id: "http://adlnet.gov/expapi/verbs/completed",
|
|
14
|
+
display: { "en-US": "completed" },
|
|
15
|
+
},
|
|
16
|
+
object: {
|
|
17
|
+
objectType: "Activity",
|
|
18
|
+
id: "http://example.com/activities/course-1",
|
|
19
|
+
definition: {
|
|
20
|
+
name: { "en-US": "Course 1" },
|
|
21
|
+
description: { "en-US": "Example course" },
|
|
22
|
+
interactionType: "choice",
|
|
23
|
+
correctResponsesPattern: ["a"],
|
|
24
|
+
choices: [{ id: "a", description: { "en-US": "Choice A" } }],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
result: {
|
|
28
|
+
score: { scaled: 1, raw: 100, min: 0, max: 100 },
|
|
29
|
+
success: true,
|
|
30
|
+
completion: true,
|
|
31
|
+
duration: "PT1H",
|
|
32
|
+
},
|
|
33
|
+
context: {
|
|
34
|
+
registration: "550e8400-e29b-41d4-a716-446655440000",
|
|
35
|
+
platform: "conform-ed",
|
|
36
|
+
language: "en-US",
|
|
37
|
+
contextActivities: {
|
|
38
|
+
parent: [{ id: "http://example.com/activities/course-1" }],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
timestamp: "2026-05-28T12:00:00.000Z",
|
|
42
|
+
version: "1.0.3",
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
test("xAPI 1.0.3 statement schema accepts the core data model", () => {
|
|
46
|
+
expect(XapiV1_0_3.Schemas.Statement.safeParse(validStatement).success).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("xAPI 2.0 statement schema accepts the same core data model", () => {
|
|
50
|
+
expect(
|
|
51
|
+
XapiV2_0.Schemas.Statement.safeParse({
|
|
52
|
+
...validStatement,
|
|
53
|
+
version: "2.0",
|
|
54
|
+
}).success,
|
|
55
|
+
).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("xAPI 2.0 statement schema accepts IEEE 2.0 contextAgent/contextGroup objects", () => {
|
|
59
|
+
const statementV2 = {
|
|
60
|
+
...validStatement,
|
|
61
|
+
version: "2.0",
|
|
62
|
+
context: {
|
|
63
|
+
...validStatement.context,
|
|
64
|
+
contextAgents: [
|
|
65
|
+
{
|
|
66
|
+
objectType: "contextAgent",
|
|
67
|
+
agent: {
|
|
68
|
+
objectType: "Agent",
|
|
69
|
+
mbox: "mailto:observer@example.com",
|
|
70
|
+
},
|
|
71
|
+
relevantTypes: ["https://example.com/xapi/relevant-types/observer"],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
contextGroups: [
|
|
75
|
+
{
|
|
76
|
+
objectType: "contextGroup",
|
|
77
|
+
group: {
|
|
78
|
+
objectType: "Group",
|
|
79
|
+
account: {
|
|
80
|
+
homePage: "https://example.com/groups",
|
|
81
|
+
name: "cohort-7",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
relevantTypes: ["https://example.com/xapi/relevant-types/cohort"],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
expect(XapiV2_0.Schemas.Statement.safeParse(statementV2).success).toBe(true);
|
|
91
|
+
expect(XapiV2_0.Schemas.StatementSubmission.safeParse([statementV2]).success).toBe(true);
|
|
92
|
+
expect(
|
|
93
|
+
XapiV2_0.Schemas.StatementResult.safeParse({
|
|
94
|
+
statements: [statementV2],
|
|
95
|
+
more: "/xapi/statements?more=page-2",
|
|
96
|
+
}).success,
|
|
97
|
+
).toBe(true);
|
|
98
|
+
expect(
|
|
99
|
+
XapiV2_0.Schemas.MultipartRequest.safeParse({
|
|
100
|
+
contentType: "multipart/mixed",
|
|
101
|
+
parts: [
|
|
102
|
+
{
|
|
103
|
+
contentType: "application/json",
|
|
104
|
+
body: statementV2,
|
|
105
|
+
},
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
"Content-Transfer-Encoding": "binary",
|
|
111
|
+
"X-Experience-API-Hash": attachmentSha2,
|
|
112
|
+
},
|
|
113
|
+
body: new Uint8Array([0x7b, 0x7d]),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
],
|
|
117
|
+
}).success,
|
|
118
|
+
).toBe(true);
|
|
119
|
+
|
|
120
|
+
expect(XapiV1_0_3.Schemas.Statement.safeParse(statementV2).success).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("xAPI statement submission schema accepts batches", () => {
|
|
124
|
+
expect(XapiV1_0_3.Schemas.StatementSubmission.safeParse([validStatement]).success).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("xAPI document, listing, and about schemas validate LRS metadata", () => {
|
|
128
|
+
expect(
|
|
129
|
+
XapiV1_0_3.Schemas.AboutResource.safeParse({
|
|
130
|
+
version: ["1.0.3"],
|
|
131
|
+
extensions: {
|
|
132
|
+
"https://example.com/xapi/lrs/extensions/features": ["person", "statement-result"],
|
|
133
|
+
},
|
|
134
|
+
}).success,
|
|
135
|
+
).toBe(true);
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
XapiV1_0_3.Schemas.StateDocumentQuery.safeParse({
|
|
139
|
+
activityId: "http://example.com/activities/course-1",
|
|
140
|
+
agent: { mbox: "mailto:alice@example.com" },
|
|
141
|
+
registration: "550e8400-e29b-41d4-a716-446655440000",
|
|
142
|
+
stateId: "progress",
|
|
143
|
+
}).success,
|
|
144
|
+
).toBe(true);
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
XapiV1_0_3.Schemas.XapiDocument.safeParse({
|
|
148
|
+
contentType: "application/json",
|
|
149
|
+
body: { progress: 0.5 },
|
|
150
|
+
etag: '"abc123"',
|
|
151
|
+
lastModified: "2026-05-28T12:00:00.000Z",
|
|
152
|
+
}).success,
|
|
153
|
+
).toBe(true);
|
|
154
|
+
|
|
155
|
+
expect(
|
|
156
|
+
XapiV1_0_3.Schemas.StateDocumentListingQuery.safeParse({
|
|
157
|
+
activityId: "http://example.com/activities/course-1",
|
|
158
|
+
agent: { mbox: "mailto:alice@example.com" },
|
|
159
|
+
since: "2026-05-27T12:00:00.000Z",
|
|
160
|
+
}).success,
|
|
161
|
+
).toBe(true);
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
XapiV1_0_3.Schemas.AgentProfileDocumentListingQuery.safeParse({
|
|
165
|
+
agent: { mbox: "mailto:alice@example.com" },
|
|
166
|
+
since: "2026-05-27T12:00:00.000Z",
|
|
167
|
+
}).success,
|
|
168
|
+
).toBe(true);
|
|
169
|
+
|
|
170
|
+
expect(
|
|
171
|
+
XapiV1_0_3.Schemas.ActivityProfileDocumentListingQuery.safeParse({
|
|
172
|
+
activityId: "http://example.com/activities/course-1",
|
|
173
|
+
since: "2026-05-27T12:00:00.000Z",
|
|
174
|
+
}).success,
|
|
175
|
+
).toBe(true);
|
|
176
|
+
|
|
177
|
+
expect(XapiV1_0_3.Schemas.XapiDocumentIdList.safeParse(["bookmark", "progress"]).success).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("xAPI validation rejects statements missing required fields", () => {
|
|
181
|
+
expect(
|
|
182
|
+
XapiV1_0_3.Schemas.Statement.safeParse({
|
|
183
|
+
verb: { id: "http://adlnet.gov/expapi/verbs/completed" },
|
|
184
|
+
object: { id: "http://example.com/activities/course-1" },
|
|
185
|
+
}).success,
|
|
186
|
+
).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("xAPI statement result, person object, and resource queries validate missing spec objects", () => {
|
|
190
|
+
expect(
|
|
191
|
+
XapiV1_0_3.Schemas.StatementResult.safeParse({
|
|
192
|
+
statements: [validStatement],
|
|
193
|
+
more: "/xapi/statements?more=opaque-token",
|
|
194
|
+
}).success,
|
|
195
|
+
).toBe(true);
|
|
196
|
+
|
|
197
|
+
expect(
|
|
198
|
+
XapiV1_0_3.Schemas.StatementResult.safeParse({
|
|
199
|
+
statements: [validStatement],
|
|
200
|
+
more: "https://example.com/xapi/statements?more=absolute",
|
|
201
|
+
}).success,
|
|
202
|
+
).toBe(false);
|
|
203
|
+
|
|
204
|
+
expect(
|
|
205
|
+
XapiV1_0_3.Schemas.Person.safeParse({
|
|
206
|
+
objectType: "Person",
|
|
207
|
+
name: ["Alice Example"],
|
|
208
|
+
mbox: ["mailto:alice@example.com"],
|
|
209
|
+
account: [
|
|
210
|
+
{
|
|
211
|
+
homePage: "https://example.com/accounts",
|
|
212
|
+
name: "alice",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
}).success,
|
|
216
|
+
).toBe(true);
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
XapiV1_0_3.Schemas.AgentsResourceQuery.safeParse({
|
|
220
|
+
agent: { mbox: "mailto:alice@example.com" },
|
|
221
|
+
}).success,
|
|
222
|
+
).toBe(true);
|
|
223
|
+
|
|
224
|
+
expect(
|
|
225
|
+
XapiV1_0_3.Schemas.ActivitiesResourceQuery.safeParse({
|
|
226
|
+
activityId: "http://example.com/activities/course-1",
|
|
227
|
+
}).success,
|
|
228
|
+
).toBe(true);
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
XapiV1_0_3.Schemas.StatementsQuery.safeParse({
|
|
232
|
+
agent: { mbox: "mailto:alice@example.com" },
|
|
233
|
+
verb: "http://adlnet.gov/expapi/verbs/completed",
|
|
234
|
+
since: "2026-05-27T12:00:00.000Z",
|
|
235
|
+
format: "canonical",
|
|
236
|
+
attachments: true,
|
|
237
|
+
ascending: false,
|
|
238
|
+
}).success,
|
|
239
|
+
).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// LRS/Content schema tests
|
|
243
|
+
test("xAPI HTTP method enum validates LRS operations", () => {
|
|
244
|
+
expect(XapiV1_0_3.Schemas.HttpMethod.safeParse("GET").success).toBe(true);
|
|
245
|
+
expect(XapiV1_0_3.Schemas.HttpMethod.safeParse("POST").success).toBe(true);
|
|
246
|
+
expect(XapiV1_0_3.Schemas.HttpMethod.safeParse("PUT").success).toBe(true);
|
|
247
|
+
expect(XapiV1_0_3.Schemas.HttpMethod.safeParse("DELETE").success).toBe(true);
|
|
248
|
+
expect(XapiV1_0_3.Schemas.HttpMethod.safeParse("PATCH").success).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("xAPI error response schema validates LRS error payloads", () => {
|
|
252
|
+
expect(
|
|
253
|
+
XapiV1_0_3.Schemas.ErrorResponse.safeParse({
|
|
254
|
+
code: "400",
|
|
255
|
+
message: "Invalid request",
|
|
256
|
+
details: "Missing required field: actor",
|
|
257
|
+
}).success,
|
|
258
|
+
).toBe(true);
|
|
259
|
+
|
|
260
|
+
expect(
|
|
261
|
+
XapiV1_0_3.Schemas.ErrorResponse.safeParse({
|
|
262
|
+
code: "500",
|
|
263
|
+
message: "Internal server error",
|
|
264
|
+
}).success,
|
|
265
|
+
).toBe(true);
|
|
266
|
+
|
|
267
|
+
expect(
|
|
268
|
+
XapiV1_0_3.Schemas.ErrorResponse.safeParse({
|
|
269
|
+
code: "400",
|
|
270
|
+
message: "Invalid request",
|
|
271
|
+
extra: "should not be here",
|
|
272
|
+
}).success,
|
|
273
|
+
).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("xAPI error codes validate standard HTTP status responses", () => {
|
|
277
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("400").success).toBe(true);
|
|
278
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("401").success).toBe(true);
|
|
279
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("403").success).toBe(true);
|
|
280
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("404").success).toBe(true);
|
|
281
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("412").success).toBe(true);
|
|
282
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("500").success).toBe(true);
|
|
283
|
+
expect(XapiV1_0_3.Schemas.ErrorCode.safeParse("999").success).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("xAPI concurrency schema validates ETags and conditional headers", () => {
|
|
287
|
+
expect(
|
|
288
|
+
XapiV1_0_3.Schemas.Concurrency.safeParse({
|
|
289
|
+
etag: '"abc123"',
|
|
290
|
+
}).success,
|
|
291
|
+
).toBe(true);
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
XapiV1_0_3.Schemas.Concurrency.safeParse({
|
|
295
|
+
ifMatch: '"abc123"',
|
|
296
|
+
}).success,
|
|
297
|
+
).toBe(true);
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
XapiV1_0_3.Schemas.Concurrency.safeParse({
|
|
301
|
+
ifNoneMatch: '"abc123"',
|
|
302
|
+
}).success,
|
|
303
|
+
).toBe(true);
|
|
304
|
+
|
|
305
|
+
expect(XapiV1_0_3.Schemas.Concurrency.safeParse({}).success).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("xAPI multipart attachment part schema validates attachment data structure", () => {
|
|
309
|
+
expect(
|
|
310
|
+
XapiV1_0_3.Schemas.MultipartAttachmentPart.safeParse({
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "image/png",
|
|
313
|
+
"Content-Transfer-Encoding": "binary",
|
|
314
|
+
"X-Experience-API-Hash": attachmentSha2,
|
|
315
|
+
},
|
|
316
|
+
body: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
317
|
+
}).success,
|
|
318
|
+
).toBe(true);
|
|
319
|
+
|
|
320
|
+
expect(
|
|
321
|
+
XapiV1_0_3.Schemas.MultipartAttachmentPart.safeParse({
|
|
322
|
+
headers: {
|
|
323
|
+
"Content-Type": "image/png",
|
|
324
|
+
"Content-Transfer-Encoding": "base64",
|
|
325
|
+
"X-Experience-API-Hash": attachmentSha2,
|
|
326
|
+
},
|
|
327
|
+
body: "iVBORw0KGgoAAAANSUhEUgAAAAUA",
|
|
328
|
+
}).success,
|
|
329
|
+
).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("xAPI multipart request schema validates statement with attachments", () => {
|
|
333
|
+
expect(
|
|
334
|
+
XapiV1_0_3.Schemas.MultipartRequest.safeParse({
|
|
335
|
+
contentType: "multipart/mixed",
|
|
336
|
+
parts: [
|
|
337
|
+
{
|
|
338
|
+
contentType: "application/json",
|
|
339
|
+
body: validStatement,
|
|
340
|
+
},
|
|
341
|
+
[
|
|
342
|
+
{
|
|
343
|
+
headers: {
|
|
344
|
+
"Content-Type": "image/png",
|
|
345
|
+
"Content-Transfer-Encoding": "binary",
|
|
346
|
+
"X-Experience-API-Hash": attachmentSha2,
|
|
347
|
+
},
|
|
348
|
+
body: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
],
|
|
352
|
+
}).success,
|
|
353
|
+
).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("xAPI resource schema models LRS endpoint definitions", () => {
|
|
357
|
+
expect(
|
|
358
|
+
XapiV1_0_3.Schemas.Resource.safeParse({
|
|
359
|
+
name: "Statements",
|
|
360
|
+
description: "Store and retrieve xAPI statements",
|
|
361
|
+
methods: ["GET", "POST", "PUT"],
|
|
362
|
+
}).success,
|
|
363
|
+
).toBe(true);
|
|
364
|
+
|
|
365
|
+
expect(
|
|
366
|
+
XapiV1_0_3.Schemas.Resource.safeParse({
|
|
367
|
+
name: "Statements",
|
|
368
|
+
description: "Store and retrieve xAPI statements",
|
|
369
|
+
methods: [],
|
|
370
|
+
}).success,
|
|
371
|
+
).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("xAPI request and response headers are properly enumerated", () => {
|
|
375
|
+
expect(XapiV1_0_3.Schemas.RequestHeader.safeParse("Authorization").success).toBe(true);
|
|
376
|
+
expect(XapiV1_0_3.Schemas.RequestHeader.safeParse("If-Match").success).toBe(true);
|
|
377
|
+
expect(XapiV1_0_3.Schemas.RequestHeader.safeParse("X-Experience-API-Hash").success).toBe(true);
|
|
378
|
+
expect(XapiV1_0_3.Schemas.RequestHeader.safeParse("X-Experience-API-Version").success).toBe(true);
|
|
379
|
+
expect(XapiV1_0_3.Schemas.RequestHeader.safeParse("Custom-Header").success).toBe(false);
|
|
380
|
+
|
|
381
|
+
expect(XapiV1_0_3.Schemas.ResponseHeader.safeParse("ETag").success).toBe(true);
|
|
382
|
+
expect(XapiV1_0_3.Schemas.ResponseHeader.safeParse("X-Experience-API-Consistent-Through").success).toBe(true);
|
|
383
|
+
expect(XapiV1_0_3.Schemas.ResponseHeader.safeParse("Authorization").success).toBe(false);
|
|
384
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Verifiable Credentials Data Model 2.0 -> Zod template notes
|
|
2
|
+
|
|
3
|
+
## Published specification
|
|
4
|
+
|
|
5
|
+
- W3C Recommendation: `https://www.w3.org/TR/vc-data-model-2.0/`
|
|
6
|
+
|
|
7
|
+
This note accompanies `packages/contracts/src/vc-data-model/v2_0/`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Scope choice
|
|
12
|
+
|
|
13
|
+
VC Data Model 2.0 does not ship as a single 1EdTech-style JSON schema bundle that maps one-to-one to this repository's versioned schema-port workflow.
|
|
14
|
+
|
|
15
|
+
To integrate it cleanly for Open Badges 3.0 and CLR 2.0, this package implements the shared VC and credential primitives used by both standards:
|
|
16
|
+
|
|
17
|
+
- `VerifiableCredentialSchema`
|
|
18
|
+
- `VerifiablePresentationSchema`
|
|
19
|
+
- `CredentialSubjectSchema`
|
|
20
|
+
- `CredentialSchemaSchema`
|
|
21
|
+
- `CredentialStatusSchema`
|
|
22
|
+
- `RefreshServiceSchema`
|
|
23
|
+
- `TermsOfUseSchema`
|
|
24
|
+
- `ProofSchema`
|
|
25
|
+
- `EvidenceSchema`
|
|
26
|
+
- `HolderSchema`
|
|
27
|
+
|
|
28
|
+
Grouped as `VcDataModel20DerivedZodTemplates`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Relationship to Open Badges and CLR
|
|
33
|
+
|
|
34
|
+
This VC Data Model surface is intentionally shared infrastructure. Open Badges 3.0 and CLR 2.0 version bundles compose on top of it, with each standard adding its own context/type constraints and domain objects.
|
|
35
|
+
|
|
36
|
+
This avoids duplicated logic and keeps cross-standard behavior aligned.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Public surface and naming
|
|
41
|
+
|
|
42
|
+
- package-root namespace: `VcDataModelV2_0`
|
|
43
|
+
- package subpath: `@conform-ed/contracts/vc-data-model/v2_0`
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# xAPI Zod templates
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This bundle covers:
|
|
6
|
+
|
|
7
|
+
- xAPI 1.0.3 statement/data models
|
|
8
|
+
- IEEE xAPI 2.0 statement/data models
|
|
9
|
+
- Statement result containers and Person objects
|
|
10
|
+
- LRS document resources (State, AgentProfile, ActivityProfile)
|
|
11
|
+
- Statement, Agents, and Activities resource query shapes
|
|
12
|
+
- Document id list responses for State/Profile resources
|
|
13
|
+
- LRS resource and endpoint schemas
|
|
14
|
+
- HTTP transport and concurrency control (ETags, conditional requests)
|
|
15
|
+
- Multipart attachment transmission with binary hashing
|
|
16
|
+
- Error handling and HTTP status codes
|
|
17
|
+
- About/version metadata
|
|
18
|
+
|
|
19
|
+
## Source references
|
|
20
|
+
|
|
21
|
+
These documents were used as normative references while modeling the xAPI payload contracts:
|
|
22
|
+
|
|
23
|
+
- ADL xAPI 1.0.3 spec documents:
|
|
24
|
+
- `xAPI-About.md`
|
|
25
|
+
- `xAPI-Data.md`
|
|
26
|
+
- `xAPI-Communication.md` (including LRS and statement submission guidance)
|
|
27
|
+
- IEEE xAPI 2.0 base-standard documents:
|
|
28
|
+
- `9274.1.1 xAPI Base Standard Front Matter.md`
|
|
29
|
+
- `9274.1.1 xAPI Base Standard Overview.md`
|
|
30
|
+
- `9274.1.1 xAPI Base Standard for Content.md`
|
|
31
|
+
- `9274.1.1 xAPI Base Standard for LRSs.md`
|
|
32
|
+
|
|
33
|
+
## Entry points
|
|
34
|
+
|
|
35
|
+
- `@conform-ed/contracts/xapi`
|
|
36
|
+
- `@conform-ed/contracts/xapi/v1_0_3`
|
|
37
|
+
- `@conform-ed/contracts/xapi/v2_0`
|
|
38
|
+
|
|
39
|
+
## Schemas
|
|
40
|
+
|
|
41
|
+
### Core Data Models
|
|
42
|
+
|
|
43
|
+
- `Agent` / `Group` — Actor types with inverse-functional identifier validation
|
|
44
|
+
- `Person` — Aggregated Agents Resource response with array-valued identifiers
|
|
45
|
+
- `Verb` — Action vocabulary with display language support
|
|
46
|
+
- `Activity` — Learning object with optional activity definition
|
|
47
|
+
- `Result` — Statement outcome with score, success, completion, and duration
|
|
48
|
+
- `Context` — Contextual information including registration, language, and activities
|
|
49
|
+
- `ContextAgent` / `ContextGroup` (IEEE 2.0) — Statement-scoped contextual agents/groups with optional `relevantTypes`
|
|
50
|
+
- `Attachment` — Binary attachment metadata with SHA2 hashing
|
|
51
|
+
- `Statement` — Complete xAPI statement combining actor, verb, object, result, and context
|
|
52
|
+
- `StatementResult` — `/statements` query response envelope containing `statements` plus optional `more`
|
|
53
|
+
|
|
54
|
+
### LRS Document Resources
|
|
55
|
+
|
|
56
|
+
- `StatementsQuery` — `/statements` query parameters (statement ids, agent/activity filters, date windows, format, attachments)
|
|
57
|
+
- `AgentsResourceQuery` — `/agents` request payload/query wrapper for a target Agent
|
|
58
|
+
- `ActivitiesResourceQuery` — `/activities` request payload/query wrapper for a target Activity id
|
|
59
|
+
- `StateDocumentQuery` — Request parameters for state document access (activityId, agent, stateId, registration)
|
|
60
|
+
- `StateDocumentListingQuery` — Request parameters for listing state ids, including optional `since`
|
|
61
|
+
- `AgentProfileDocumentQuery` — Request parameters for agent profile access
|
|
62
|
+
- `AgentProfileDocumentListingQuery` — Request parameters for listing agent profile ids
|
|
63
|
+
- `ActivityProfileDocumentQuery` — Request parameters for activity profile access
|
|
64
|
+
- `ActivityProfileDocumentListingQuery` — Request parameters for listing activity profile ids
|
|
65
|
+
- `XapiDocument` — Response envelope for document retrieval (contentType, body, etag, lastModified)
|
|
66
|
+
- `XapiDocumentIdList` — Top-level JSON array response for State/Profile id listings
|
|
67
|
+
|
|
68
|
+
### Transport and Concurrency
|
|
69
|
+
|
|
70
|
+
- `HttpMethod` — Enumeration of LRS HTTP methods (GET, HEAD, PUT, POST, DELETE)
|
|
71
|
+
- `RequestHeader` — HTTP request headers supported by xAPI (Authorization, If-Match, etc.)
|
|
72
|
+
- `ResponseHeader` — HTTP response headers (ETag, X-Experience-API-Version, etc.)
|
|
73
|
+
- `Concurrency` — Concurrency control with ETag and conditional request headers (If-Match, If-None-Match)
|
|
74
|
+
- `ErrorCode` — Standard HTTP error codes returned by LRS (400, 401, 403, 404, 409, 411, 412, 413, 500, 501)
|
|
75
|
+
- `ErrorResponse` — Error payload with code, message, and optional details
|
|
76
|
+
|
|
77
|
+
### Multipart Attachments
|
|
78
|
+
|
|
79
|
+
- `MultipartAttachmentPart` — Binary attachment data with required Content-Type, Content-Transfer-Encoding (binary), and X-Experience-API-Hash headers
|
|
80
|
+
- `MultipartRequest` — Full multipart/mixed request envelope with JSON statement(s) followed by binary attachment parts
|
|
81
|
+
|
|
82
|
+
### Service Metadata
|
|
83
|
+
|
|
84
|
+
- `AboutResource` — LRS about/version endpoint response listing supported xAPI versions
|
|
85
|
+
- `Resource` — Endpoint definition with name, description, and supported HTTP methods
|
|
86
|
+
|
|
87
|
+
## Notes
|
|
88
|
+
|
|
89
|
+
- Structured statement objects and all domain types are strict (`strictObject()`), catching misspelled or extra properties.
|
|
90
|
+
- Document bodies are intentionally permissive (`z.unknown()`) because xAPI allows arbitrary document content types.
|
|
91
|
+
- Agent and Group schemas enforce at-least-one-identifier validation via `.refine()`, matching the spec intent that agents must be identifiable.
|
|
92
|
+
- Person objects are modeled separately from Agents because the `/agents` resource returns array-valued identifier fields rather than single IFIs.
|
|
93
|
+
- Concurrency control and multipart transmission are modeled as separate concerns for flexibility in HTTP client implementations.
|
|
94
|
+
- Version 2.0 reuses the 1.0.3 model where structurally equivalent, but overrides Context/Statement-family schemas to add IEEE `contextAgent`, `contextGroup`, and `relevantTypes`.
|
|
95
|
+
- Strict error codes (400, 401, 403, 404, 409, 411, 412, 413, 500, 501) align with xAPI specification guidance for LRS error responses.
|