From 4da1e245078d2f4da9972ac56471d2dceca49331 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 28 Sep 2026 14:48:56 +1000 Subject: [PATCH] feat: carry the MAP 0.2 core artifacts --- README.md | 56 +- UPSTREAM.md | 13 +- artifacts.json | 7 + bin/mailschema.js | 67 +- build.mjs | 14 +- dist/content-review-0.1.contract.json | 58 -- dist/content-review-0.1.schema.json | 54 -- dist/content-review-0.2.contract.json | 91 --- dist/content-review-0.2.schema.json | 54 -- dist/forms-0.1.schema.json | 329 ++++++++ dist/index.d.ts | 2 +- dist/index.js | 4 +- dist/map-0.1.schema.json | 411 ---------- dist/map-0.2.jsonld | 91 +++ dist/map-0.2.schema.json | 1033 +++++++++++++++++++++++++ dist/map.d.ts | 19 +- dist/map.js | 67 +- dist/type-contract-0.2.schema.json | 179 +++++ package-lock.json | 22 +- package.json | 16 +- src/content-review-0.1.contract.json | 58 -- src/content-review-0.1.schema.json | 54 -- src/content-review-0.2.contract.json | 91 --- src/content-review-0.2.schema.json | 54 -- src/forms-0.1.schema.json | 329 ++++++++ src/index.ts | 16 +- src/map-0.1.schema.json | 411 ---------- src/map-0.2.jsonld | 91 +++ src/map-0.2.schema.json | 1033 +++++++++++++++++++++++++ src/map.ts | 74 +- src/type-contract-0.2.schema.json | 179 +++++ test/map-description.json | 53 -- test/map-request.json | 18 - test/map-result.json | 23 - test/new-type.json | 55 ++ test/package.test.mjs | 45 +- 36 files changed, 3480 insertions(+), 1691 deletions(-) create mode 100644 artifacts.json delete mode 100644 dist/content-review-0.1.contract.json delete mode 100644 dist/content-review-0.1.schema.json delete mode 100644 dist/content-review-0.2.contract.json delete mode 100644 dist/content-review-0.2.schema.json create mode 100644 dist/forms-0.1.schema.json delete mode 100644 dist/map-0.1.schema.json create mode 100644 dist/map-0.2.jsonld create mode 100644 dist/map-0.2.schema.json create mode 100644 dist/type-contract-0.2.schema.json delete mode 100644 src/content-review-0.1.contract.json delete mode 100644 src/content-review-0.1.schema.json delete mode 100644 src/content-review-0.2.contract.json delete mode 100644 src/content-review-0.2.schema.json create mode 100644 src/forms-0.1.schema.json delete mode 100644 src/map-0.1.schema.json create mode 100644 src/map-0.2.jsonld create mode 100644 src/map-0.2.schema.json create mode 100644 src/type-contract-0.2.schema.json delete mode 100644 test/map-description.json delete mode 100644 test/map-request.json delete mode 100644 test/map-result.json create mode 100644 test/new-type.json diff --git a/README.md b/README.md index 19b1c01..da50583 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![npm](https://img.shields.io/npm/v/mailschema)](https://www.npmjs.com/package/mailschema) [![CI](https://github.com/mailschema/javascript/actions/workflows/test.yml/badge.svg)](https://github.com/mailschema/javascript/actions/workflows/test.yml) -Validate Mail Action Protocol documents and MailSchema Registry contributions without making a network request. +The Mail Action Protocol 0.2 core artifacts, and local validation of MailSchema Registry contributions, with no network requests. -[Specification](https://mailschema.org/specification/) · [Registry](https://mailschema.org/registry/) · [Tools](https://mailschema.org/tools/) · [Source](https://github.com/mailschema/javascript) +[Specification](https://mailschema.org/specification) · [Registry](https://mailschema.org/registry) · [Tools](https://mailschema.org/tools) · [Source](https://github.com/mailschema/javascript) ## Install @@ -15,62 +15,40 @@ npm install mailschema Node.js 22 or newer is required. The package is ESM and includes TypeScript declarations. -## Validate MAP documents +## MAP 0.2 core artifacts ```js import { - assertMapDocument, - assertContentReviewRequest, - getContentReviewContract, + MAP_PROFILE, + getContractFormatSchema, + getFormsSchema, + getMapContext, getMapSchema, } from 'mailschema'; - -assertMapDocument(description); -assertContentReviewRequest(request); -const mapSchema = getMapSchema(); -const contentReviewContract = getContentReviewContract(); ``` -The CLI performs the same checks against local JSON files: - -```sh -npx mailschema check description.json --map -npx mailschema check request.json --content-review -``` +Each is byte-identical to the file the [profile record](https://mailschema.org/profiles/map/0.2.json) binds by SHA-256, and is also exported raw as `mailschema/map-0.2.schema.json`, `mailschema/map-0.2.jsonld`, `mailschema/type-contract-0.2.schema.json` and `mailschema/forms-0.1.schema.json`. Type contracts are not bundled: a client obtains them from the [Registry catalogue](https://mailschema.org/registry/catalog.json) by digest. Processing MAP messages is outside this package; see the [profile](https://mailschema.org/specification/profile). -## Work with Registry data +## Registry data ```js -import { - assertContribution, - contributionErrors, - getContributionSchema, - referenceErrors, -} from 'mailschema'; +import { assertContribution, contributionErrors, referenceErrors } from 'mailschema'; const errors = contributionErrors(candidate); if (errors.length) console.error(errors); else assertContribution(candidate); -const schema = getContributionSchema(); const referenceProblems = referenceErrors(candidate, catalog); ``` -`assertTypeRecord`, `getRecordSchema` and `mailschema check record.json --record` handle expanded Registry records. Reference checks use a catalogue supplied by the caller; the package never fetches one automatically. +`assertTypeRecord` and `getRecordSchema` handle expanded Registry records. Reference checks use a catalogue supplied by the caller; the package never fetches one. -Raw Draft 2020-12 schemas are exported as: - -- `mailschema/map-0.1.schema.json` -- `mailschema/content-review-0.1.schema.json` -- `mailschema/content-review-0.1.contract.json` -- `mailschema/content-review-0.2.schema.json` -- `mailschema/content-review-0.2.contract.json` -- `mailschema/contribution.schema.json` - -The unversioned Content Review helpers use 0.2. The `*ContentReview01*` helpers and 0.1 raw exports remain available for implementations pinned to the earlier draft. +```sh +npx mailschema check contribution.json +npx mailschema check record.json --record +npx mailschema schema --map +``` ## Trust boundary -A valid document is structured input. Validation does not authenticate a service, grant authority, approve an action or establish product conformance. Implementations must apply their own endpoint trust, credentials, permissions and policy before executing a request. - -The CLI reads one local file of at most 256 KiB and does not upload or modify it. Package versions and MAP profile versions advance independently. MIT licensed. +A valid document is structured input. Validation does not authenticate a service, grant authority or establish product conformance. The CLI reads one local file of at most 256 KiB and does not upload or modify it. Package versions and MAP profile versions advance independently. MIT licensed. diff --git a/UPSTREAM.md b/UPSTREAM.md index 7c880c4..55a6f8e 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -1,14 +1,13 @@ # Release provenance -Version `0.1.4` derives from [`mailschema/mailschema@f630ec5`](https://github.com/mailschema/mailschema/commit/f630ec5569e7167114fb2a39a10548febfa6b979). +Version `0.1.5` derives from [`mailschema/mailschema@61a12c9`](https://github.com/mailschema/mailschema/commit/61a12c97663061b46707f8d996bd1f59cae9a544), in [pull request #22](https://github.com/mailschema/mailschema/pull/22). The bundled files are exact projections of that source commit and do not independently define MAP: -- MAP 0.1 schema: `c39befeecf8643e8beda1f53a8d1411482b68ddf14974f3806cfa95776103f85` -- Content Review 0.1 schema: `10e2263c820ba3a6ef0381c377078e2c1f324c3b142a6f8caf92b298cc66c692` -- Content Review 0.1 contract: `754ab7245ddb09ff4da000fccc60d746724379d4146ea227973e5d87f105f800` -- Content Review 0.2 schema: `d2286ffe410cceae56f9caa145634fd9613f5ce51e11ba089cad67b9f62259b7` -- Content Review 0.2 contract: `3bd1cd4a86e00d00590eeb150b7b29cebece589058d6094a40aa9d497dc8efe0` +- MAP 0.2 core schema: `732d60fcb758d945dc15c57d885c9d1c0c76423a7439096d9755e8ec9830836a` +- MAP 0.2 JSON-LD context: `dfed0189524d7d16d31ed3d8af97e9a0c458200c4c034b413f9d14fa35b60257` +- Type contract format: `e030007cc034662c9d9d448a8a035aff3106cfb96fb60e6aa9f92b99b25df0b9` +- Form fields block: `3a976b93a057380e3630ca05e77e221a43a98559fb7fda8066299ec4bf2a8f9e` - Contribution schema: `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564` -The canonical contracts live in the main MailSchema repository. This repository owns the JavaScript and TypeScript package surface and its release history. +The canonical artifacts live in the main MailSchema repository. This repository owns the JavaScript and TypeScript package surface and its release history. diff --git a/artifacts.json b/artifacts.json new file mode 100644 index 0000000..cb8b4ff --- /dev/null +++ b/artifacts.json @@ -0,0 +1,7 @@ +[ + "contribution.schema.json", + "map-0.2.schema.json", + "map-0.2.jsonld", + "type-contract-0.2.schema.json", + "forms-0.1.schema.json" +] diff --git a/bin/mailschema.js b/bin/mailschema.js index e903eb0..d8c83e4 100755 --- a/bin/mailschema.js +++ b/bin/mailschema.js @@ -3,59 +3,52 @@ import { open } from 'node:fs/promises'; import { parseArgs } from 'node:util'; import { assertContribution, - assertContentReviewRequest, - assertMapDocument, assertTypeRecord, - getContentReviewSchema, + getContractFormatSchema, getContributionSchema, + getFormsSchema, + getMapContext, getMapSchema, getRecordSchema, } from '../dist/index.js'; -const help = `MailSchema contribution tools +const help = `MailSchema Registry tools - mailschema check [--record | --map | --content-review] - mailschema schema [--record | --map | --content-review] + mailschema check [--record] + mailschema schema [--record | --map | --context | --contract-format | --forms] -Checks JSON structure and required fields locally. Registry references and -editorial acceptance are separate checks. No files are uploaded or changed.`; +check validates a Registry contribution, or with --record an expanded record, +locally. schema prints a bundled schema: the contribution schema by default, or +a MAP 0.2 core artifact. No files are uploaded or changed.`; + +const schemas = { + record: getRecordSchema, + map: getMapSchema, + context: getMapContext, + 'contract-format': getContractFormatSchema, + forms: getFormsSchema, +}; try { const { values, positionals } = parseArgs({ allowPositionals: true, options: { - record: { type: 'boolean' }, - map: { type: 'boolean' }, - 'content-review': { type: 'boolean' }, + ...Object.fromEntries(Object.keys(schemas).map((name) => [name, { type: 'boolean' }])), help: { type: 'boolean', short: 'h' }, }, }); const [command, path, ...extra] = positionals; - const formats = [values.record, values.map, values['content-review']].filter(Boolean); - if (formats.length > 1) throw new Error('Choose only one document format.'); - const selected = values['content-review'] - ? 'Content Review request' - : values.map - ? 'MAP document' - : values.record - ? 'type record' - : 'contribution'; + const chosen = Object.keys(schemas).filter((name) => values[name]); + if (chosen.length > 1) throw new Error('Choose only one schema.'); if (values.help || !command) console.log(help); else if (command === 'schema' && !path && !extra.length) - console.log( - JSON.stringify( - values['content-review'] - ? getContentReviewSchema() - : values.map - ? getMapSchema() - : values.record - ? getRecordSchema() - : getContributionSchema(), - null, - 2, - ), - ); - else if (command === 'check' && path && !extra.length) { + console.log(JSON.stringify((schemas[chosen[0]] ?? getContributionSchema)(), null, 2)); + else if ( + command === 'check' && + path && + !extra.length && + chosen.every((name) => name === 'record') + ) { const file = await open(path, 'r'); let value; try { @@ -69,11 +62,9 @@ try { } finally { await file.close(); } - if (values['content-review']) assertContentReviewRequest(value); - else if (values.map) assertMapDocument(value); - else if (values.record) assertTypeRecord(value); + if (values.record) assertTypeRecord(value); else assertContribution(value); - console.log(`Valid MailSchema ${selected}.`); + console.log(`Valid MailSchema ${values.record ? 'type record' : 'contribution'}.`); } else throw new Error(help); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); diff --git a/build.mjs b/build.mjs index c7da4d6..57fae15 100644 --- a/build.mjs +++ b/build.mjs @@ -1,5 +1,5 @@ import { execFileSync } from 'node:child_process'; -import { copyFile } from 'node:fs/promises'; +import { copyFile, readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -10,13 +10,9 @@ execFileSync(process.execPath, [resolve(root, 'node_modules/typescript/bin/tsc') stdio: 'inherit', }); -for (const name of [ - 'contribution.schema.json', - 'map-0.1.schema.json', - 'content-review-0.1.schema.json', - 'content-review-0.1.contract.json', - 'content-review-0.2.schema.json', - 'content-review-0.2.contract.json', -]) { +const artifacts = JSON.parse(await readFile(resolve(root, 'artifacts.json'), 'utf8')); +if (!Array.isArray(artifacts) || artifacts.some((name) => typeof name !== 'string')) + throw new Error('Invalid generated package artifact list.'); +for (const name of artifacts) { await copyFile(resolve(root, 'src', name), resolve(root, 'dist', name)); } diff --git a/dist/content-review-0.1.contract.json b/dist/content-review-0.1.contract.json deleted file mode 100644 index 922baef..0000000 --- a/dist/content-review-0.1.contract.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/content-review", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.1", - "target": "Service-owned content at the exact revision and digest named in the request.", - "requestSchema": { - "url": "https://mailschema.org/schemas/content-review-0.1.schema.json", - "canonicalDigest": "sha-256:cdad1149ab193d1bf71cb9463e3c4b7331ed56b0123e225adf68f70a2022b29d" - }, - "operations": [ - { - "id": "request-changes", - "effect": "Record review feedback on the named revision.", - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedbackRecorded": { "const": true } - }, - "required": ["feedbackRecorded"] - } - } - ] - }, - { - "id": "approve", - "effect": "Record approval of the named revision.", - "results": [ - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "approvalUrl": { "type": "string", "format": "uri", "pattern": "^https://" } - }, - "required": ["approvalUrl"] - } - }, - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { "const": "approved" } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/dist/content-review-0.1.schema.json b/dist/content-review-0.1.schema.json deleted file mode 100644 index 57fc90f..0000000 --- a/dist/content-review-0.1.schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/content-review-0.1.schema.json", - "title": "Content Review 0.1 MAP request binding", - "allOf": [ - { "$ref": "https://mailschema.org/schemas/map-0.1.schema.json#/$defs/request" }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { "const": "https://mailschema.org/types/content-review" }, - "version": { "const": "0.1" } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { "const": "request-changes" }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedback": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["feedback"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { "const": "approve" }, - "input": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - } - } - } - ] - } - ] -} diff --git a/dist/content-review-0.2.contract.json b/dist/content-review-0.2.contract.json deleted file mode 100644 index 7dec56a..0000000 --- a/dist/content-review-0.2.contract.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/content-review", - "version": "0.2", - "profile": "https://mailschema.org/profiles/map/0.1", - "target": "Service-owned content at the exact revision and digest named in the request.", - "requestSchema": { - "url": "https://mailschema.org/schemas/content-review-0.2.schema.json", - "canonicalDigest": "sha-256:43a01e620e0730e47755694bdf5d086f8d0a1d3d8328fb0c7d2a4b70c6e62e25" - }, - "operations": [ - { - "id": "request-changes", - "effect": "Record review feedback on the named revision.", - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedbackRecorded": { - "const": true - } - }, - "required": [ - "feedbackRecorded" - ] - } - } - ] - }, - { - "id": "approve", - "effect": "Record approval of the named revision, or record why a proposed approval ended without approval.", - "results": [ - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "approvalUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://" - } - }, - "required": [ - "approvalUrl" - ] - } - }, - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "approved" - } - }, - "required": [ - "decision" - ] - } - }, - { - "state": "failed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "enum": [ - "declined", - "stale-target", - "expired" - ] - } - }, - "required": [ - "reason" - ] - } - } - ] - } - ] -} diff --git a/dist/content-review-0.2.schema.json b/dist/content-review-0.2.schema.json deleted file mode 100644 index 88b5be7..0000000 --- a/dist/content-review-0.2.schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/content-review-0.2.schema.json", - "title": "Content Review 0.2 MAP request binding", - "allOf": [ - { "$ref": "https://mailschema.org/schemas/map-0.1.schema.json#/$defs/request" }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { "const": "https://mailschema.org/types/content-review" }, - "version": { "const": "0.2" } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { "const": "request-changes" }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedback": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["feedback"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { "const": "approve" }, - "input": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - } - } - } - ] - } - ] -} diff --git a/dist/forms-0.1.schema.json b/dist/forms-0.1.schema.json new file mode 100644 index 0000000..88f680b --- /dev/null +++ b/dist/forms-0.1.schema.json @@ -0,0 +1,329 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/forms-0.1.schema.json", + "title": "MailSchema form fields 0.1", + "description": "Shared, immutable blocks for types that collect values: a closed subset of JSON Schema based on Model Context Protocol elicitation, with HTML autofill field names.", + "$defs": { + "fieldName": { + "type": "string", + "pattern": "^[a-z][A-Za-z0-9]{0,63}$" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "autocomplete": { + "type": "string", + "maxLength": 200, + "pattern": "^(?:section-[a-z0-9-]+ )?(?:(?:shipping|billing) )?(?:(?:home|work|mobile|fax|pager) )?[a-z][a-z0-9-]*$", + "not": { + "pattern": "(?:^| )(?:current-password|new-password|one-time-code|cc-[a-z-]+)$" + } + }, + "choice": { + "type": "object", + "additionalProperties": false, + "properties": { + "const": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + } + }, + "required": ["const", "title"] + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/$defs/choice" + }, + "minItems": 1, + "maxItems": 100 + }, + "textField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "string" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "minLength": { + "type": "integer", + "minimum": 0, + "maximum": 12000 + }, + "maxLength": { + "type": "integer", + "minimum": 1, + "maximum": 12000 + }, + "format": { + "enum": ["email", "uri", "date", "date-time"] + }, + "default": { + "type": "string", + "maxLength": 12000 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title"] + }, + "numberField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": ["number", "integer"] + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "minimum": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "maximum": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "default": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title"] + }, + "booleanField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "boolean" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "default": { + "type": "boolean" + } + }, + "required": ["type", "title"] + }, + "choiceField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "string" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "oneOf": { + "$ref": "#/$defs/choices" + }, + "default": { + "type": "string", + "maxLength": 200 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title", "oneOf"] + }, + "choicesField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "array" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "anyOf": { + "$ref": "#/$defs/choices" + } + }, + "required": ["anyOf"] + }, + "minItems": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "maxItems": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "uniqueItems": { + "const": true + }, + "default": { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + "required": ["type", "title", "items", "uniqueItems"] + }, + "fields": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "object" + }, + "properties": { + "type": "object", + "minProperties": 1, + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/$defs/textField" + }, + { + "$ref": "#/$defs/numberField" + }, + { + "$ref": "#/$defs/booleanField" + }, + { + "$ref": "#/$defs/choiceField" + }, + { + "$ref": "#/$defs/choicesField" + } + ] + } + }, + "required": { + "type": "array", + "items": { + "$ref": "#/$defs/fieldName" + }, + "uniqueItems": true, + "maxItems": 50 + } + }, + "required": ["type", "properties"] + }, + "choiceFields": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "object" + }, + "properties": { + "type": "object", + "minProperties": 1, + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/$defs/booleanField" + }, + { + "$ref": "#/$defs/choiceField" + }, + { + "$ref": "#/$defs/choicesField" + } + ] + } + }, + "required": { + "type": "array", + "items": { + "$ref": "#/$defs/fieldName" + }, + "uniqueItems": true, + "maxItems": 50 + } + }, + "required": ["type", "properties"] + }, + "fieldValues": { + "type": "object", + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 12000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 100, + "uniqueItems": true + } + ] + } + } + } +} diff --git a/dist/index.d.ts b/dist/index.d.ts index ddcc072..49ef88b 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -2,7 +2,7 @@ export { contributionErrors, assertContribution, assertTypeRecord, referenceErro export type { CatalogView } from './validation.js'; export { typeStages } from './model.js'; export type { Party, TypeDefinition, TypeRecord, Contribution, Implementation } from './model.js'; -export { mapErrors, assertMapDocument, contentReviewRequestErrors, assertContentReviewRequest, contentReview01RequestErrors, assertContentReview01Request, getMapSchema, getContentReviewSchema, getContentReviewContract, getContentReview01Schema, getContentReview01Contract, } from './map.js'; +export { MAP_PROFILE, getMapSchema, getMapContext, getContractFormatSchema, getFormsSchema, } from './map.js'; /** Return an independent copy of the contribution JSON Schema (Draft 2020-12). */ export declare function getContributionSchema(): Record; /** Return the schema for an expanded Registry record, including attribution and history. */ diff --git a/dist/index.js b/dist/index.js index 7538a70..eeef22d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,8 +1,8 @@ -/** Schemas and validation tools for MAP documents and MailSchema Registry contributions. */ +/** The MAP 0.2 core artifacts and validation tools for MailSchema Registry contributions. */ import schema from './contribution.schema.json' with { type: 'json' }; export { contributionErrors, assertContribution, assertTypeRecord, referenceErrors, } from './validation.js'; export { typeStages } from './model.js'; -export { mapErrors, assertMapDocument, contentReviewRequestErrors, assertContentReviewRequest, contentReview01RequestErrors, assertContentReview01Request, getMapSchema, getContentReviewSchema, getContentReviewContract, getContentReview01Schema, getContentReview01Contract, } from './map.js'; +export { MAP_PROFILE, getMapSchema, getMapContext, getContractFormatSchema, getFormsSchema, } from './map.js'; /** Return an independent copy of the contribution JSON Schema (Draft 2020-12). */ export function getContributionSchema() { return structuredClone(schema); diff --git a/dist/map-0.1.schema.json b/dist/map-0.1.schema.json deleted file mode 100644 index 352cd5d..0000000 --- a/dist/map-0.1.schema.json +++ /dev/null @@ -1,411 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/map-0.1.schema.json", - "title": "Mail Action Protocol 0.1", - "description": "Description, request, result and problem documents for the MAP 0.1 authenticated HTTPS profile.", - "oneOf": [ - { "$ref": "#/$defs/description" }, - { "$ref": "#/$defs/request" }, - { "$ref": "#/$defs/result" }, - { "$ref": "#/$defs/problem" } - ], - "$defs": { - "profile": { - "const": "https://mailschema.org/profiles/map/0.1" - }, - "identifier": { - "type": "string", - "format": "uri", - "maxLength": 2048 - }, - "httpsIdentifier": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "uuidUrn": { - "type": "string", - "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "digest": { - "type": "string", - "pattern": "^sha-256:[a-f0-9]{64}$" - }, - "contractDigest": { - "type": "string", - "pattern": "^sha-256:[a-f0-9]{64}$" - }, - "typeReference": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - "contractDigest": { "$ref": "#/$defs/contractDigest" } - }, - "required": ["id", "version", "contractDigest"] - }, - "target": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "revision": { "type": "string", "minLength": 1, "maxLength": 240 }, - "title": { "type": "string", "minLength": 1, "maxLength": 500 }, - "digest": { "$ref": "#/$defs/digest" } - }, - "required": ["id", "revision", "digest"] - }, - "operation": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { "type": "string", "minLength": 1, "maxLength": 240 }, - "description": { "type": "string", "minLength": 1, "maxLength": 2000 }, - "inputSchema": { "$ref": "#/$defs/identifier" } - }, - "required": ["id", "name", "description", "inputSchema"] - }, - "description": { - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "const": "https://mailschema.org/contexts/map-0.1.jsonld" - }, - "@type": { "const": "MailAction" }, - "@id": { "$ref": "#/$defs/uuidUrn" }, - "profile": { "$ref": "#/$defs/profile" }, - "type": { "$ref": "#/$defs/typeReference" }, - "describedAt": { "type": "string", "format": "date-time" }, - "expiresAt": { "type": "string", "format": "date-time" }, - "service": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "name": { "type": "string", "minLength": 1, "maxLength": 240 }, - "execution": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { "$ref": "#/$defs/httpsIdentifier" }, - "method": { "const": "POST" }, - "requestMediaType": { "const": "application/json" }, - "resultMediaType": { "const": "application/json" }, - "resultUrlTemplate": { - "type": "string", - "format": "uri-template", - "pattern": "^https://.*\\{requestId\\}.*$", - "maxLength": 2048 - }, - "resultRetentionSeconds": { - "type": "integer", - "minimum": 300, - "maximum": 31536000 - } - }, - "required": [ - "url", - "method", - "requestMediaType", - "resultMediaType", - "resultUrlTemplate", - "resultRetentionSeconds" - ] - }, - "humanUrl": { "$ref": "#/$defs/httpsIdentifier" }, - "authorization": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "service-configured" }, - "schemes": { - "type": "array", - "items": { "enum": ["oauth2", "bearer", "session"] }, - "minItems": 1, - "uniqueItems": true - }, - "audience": { "type": "string", "minLength": 1, "maxLength": 500 } - }, - "required": ["kind", "schemes"] - } - }, - "required": ["id", "name", "execution", "humanUrl", "authorization"] - }, - "target": { "$ref": "#/$defs/target" }, - "operations": { - "type": "array", - "items": { "$ref": "#/$defs/operation" }, - "minItems": 1, - "maxItems": 100 - } - }, - "required": [ - "@context", - "@type", - "@id", - "profile", - "type", - "describedAt", - "expiresAt", - "service", - "target", - "operations" - ] - }, - "request": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "MapRequest" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "type": { "$ref": "#/$defs/typeReference" }, - "operation": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "target": { "$ref": "#/$defs/target" }, - "input": { "type": "object", "maxProperties": 100 } - }, - "required": [ - "kind", - "profile", - "requestId", - "interactionId", - "type", - "operation", - "target", - "input" - ] - }, - "result": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "MapResult" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "type": { "$ref": "#/$defs/typeReference" }, - "operation": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "state": { - "enum": ["accepted", "completed", "failed", "pending", "approval-required"] - }, - "target": { "$ref": "#/$defs/target" }, - "recordedAt": { "type": "string", "format": "date-time" }, - "resultUrl": { "$ref": "#/$defs/httpsIdentifier" }, - "output": { "type": "object", "maxProperties": 100 } - }, - "required": [ - "kind", - "profile", - "requestId", - "interactionId", - "type", - "operation", - "state", - "target", - "recordedAt", - "resultUrl", - "output" - ] - }, - "problem": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "enum": [ - "https://mailschema.org/problems/refused", - "https://mailschema.org/problems/authentication-required", - "https://mailschema.org/problems/result-not-found", - "https://mailschema.org/problems/stale-target", - "https://mailschema.org/problems/expired-interaction", - "https://mailschema.org/problems/invalid-request", - "https://mailschema.org/problems/unsupported-profile", - "https://mailschema.org/problems/unsupported-type", - "https://mailschema.org/problems/unsupported-operation", - "https://mailschema.org/problems/idempotency-conflict", - "https://mailschema.org/problems/request-in-progress" - ] - }, - "title": { "type": "string", "minLength": 1, "maxLength": 240 }, - "status": { "type": "integer", "minimum": 400, "maximum": 599 }, - "detail": { "type": "string", "minLength": 1, "maxLength": 4000 }, - "instance": { "$ref": "#/$defs/identifier" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "code": { - "enum": [ - "refused", - "authentication-required", - "result-not-found", - "stale-target", - "expired-interaction", - "invalid-request", - "unsupported-profile", - "unsupported-type", - "unsupported-operation", - "idempotency-conflict", - "request-in-progress" - ] - }, - "target": { "$ref": "#/$defs/target" } - }, - "required": ["type", "title", "status", "detail"], - "dependentRequired": { - "instance": ["profile", "requestId", "code"], - "profile": ["instance", "requestId", "code"], - "requestId": ["instance", "profile", "code"], - "interactionId": ["instance", "profile", "requestId", "code"], - "code": ["instance", "profile", "requestId"] - }, - "allOf": [ - { - "if": { - "properties": { "code": { "not": { "const": "result-not-found" } } }, - "required": ["code"] - }, - "then": { "required": ["interactionId"] } - }, - { - "if": { "properties": { "code": { "const": "invalid-request" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/invalid-request" }, - "status": { "const": 400 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "authentication-required" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/authentication-required" }, - "status": { "const": 401 } - } - } - }, - { - "if": { "properties": { "code": { "const": "refused" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/refused" }, - "status": { "const": 403 } - } - } - }, - { - "if": { "properties": { "code": { "const": "result-not-found" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/result-not-found" }, - "status": { "const": 404 } - } - } - }, - { - "if": { "properties": { "code": { "const": "stale-target" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/stale-target" }, - "status": { "const": 409 } - }, - "required": ["target"] - } - }, - { - "if": { - "properties": { "code": { "const": "idempotency-conflict" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/idempotency-conflict" }, - "status": { "const": 409 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "request-in-progress" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/request-in-progress" }, - "status": { "const": 409 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "expired-interaction" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/expired-interaction" }, - "status": { "const": 410 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "unsupported-profile" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-profile" }, - "status": { "const": 422 } - } - } - }, - { - "if": { "properties": { "code": { "const": "unsupported-type" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-type" }, - "status": { "const": 422 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "unsupported-operation" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-operation" }, - "status": { "const": 422 } - } - } - } - ] - } - } -} diff --git a/dist/map-0.2.jsonld b/dist/map-0.2.jsonld new file mode 100644 index 0000000..c97aca3 --- /dev/null +++ b/dist/map-0.2.jsonld @@ -0,0 +1,91 @@ +{ + "@context": { + "@version": 1.1, + "map": "https://mailschema.org/ns/map#", + "schema": "https://schema.org/", + "MailAction": "map:MailAction", + "profile": { + "@id": "map:profile", + "@type": "@id" + }, + "type": { + "@id": "map:interactionType", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "version": "schema:version", + "contractDigest": "map:contractDigest", + "describedAt": { + "@id": "schema:dateCreated", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expiresAt": { + "@id": "schema:expires", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "service": { + "@id": "schema:provider", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "name": "schema:name", + "authority": "map:authority", + "onBehalfOf": { + "@id": "map:onBehalfOf", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "resource": { + "@id": "map:resource", + "@type": "@id" + }, + "execution": "map:execution", + "url": { + "@id": "schema:url", + "@type": "@id" + }, + "resultUrlTemplate": "map:resultUrlTemplate", + "resultRetentionSeconds": "map:resultRetentionSeconds", + "humanUrl": { + "@id": "map:humanUrl", + "@type": "@id" + }, + "target": { + "@id": "schema:object", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "recipient": "schema:recipient", + "details": { + "@id": "map:details", + "@type": "@json" + }, + "revision": "schema:version", + "title": "schema:name", + "digest": "map:digest", + "operations": { + "@id": "schema:potentialAction", + "@container": "@set", + "@context": { + "id": "map:operationId" + } + }, + "description": "schema:description" + } +} diff --git a/dist/map-0.2.schema.json b/dist/map-0.2.schema.json new file mode 100644 index 0000000..a87b566 --- /dev/null +++ b/dist/map-0.2.schema.json @@ -0,0 +1,1033 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/map-0.2.schema.json", + "title": "Mail Action Protocol 0.2", + "description": "Description, request, result and problem documents for MAP 0.2: a type-agnostic core whose types are defined by digest-bound contracts, with credential or possession authority over HTTPS.", + "oneOf": [ + { + "$ref": "#/$defs/description" + }, + { + "$ref": "#/$defs/request" + }, + { + "$ref": "#/$defs/result" + }, + { + "$ref": "#/$defs/problem" + } + ], + "$defs": { + "profile": { + "const": "https://mailschema.org/profiles/map/0.2" + }, + "nonBlank": { + "type": "string", + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "dateTime": { + "type": "string", + "pattern": "^(?:[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,3})?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$" + }, + "date": { + "type": "string", + "pattern": "^(?:[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)$" + }, + "identifier": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+$", + "maxLength": 2048 + }, + "httpsIdentifier": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?#](?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*)?$", + "maxLength": 2048 + }, + "httpsTarget": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?](?:[A-Za-z0-9._~:/?@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*)?$", + "maxLength": 2048 + }, + "httpsOrigin": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$", + "maxLength": 300 + }, + "urlTemplate": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?[/?](?:[A-Za-z0-9._~:/?@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*\\{requestId\\}(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*$", + "maxLength": 2048 + }, + "address": { + "type": "string", + "pattern": "^(?:[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]|[^\\u0000-\\u00a0\\u1680\\u2000-\\u200f\\u2028-\\u202f\\u205f-\\u206f\\u3000\\ufeff])+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", + "maxLength": 320 + }, + "uuidUrn": { + "type": "string", + "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + }, + "contractDigest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + }, + "typeReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", + "maxLength": 80 + }, + "contractDigest": { + "$ref": "#/$defs/contractDigest" + } + }, + "required": ["id", "version", "contractDigest"] + }, + "token": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 100 + }, + "target": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "digest": { + "$ref": "#/$defs/digest" + } + }, + "required": ["id", "revision", "digest"] + }, + "operation": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/token" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": ["id", "name", "description"] + }, + "description": { + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "const": "https://mailschema.org/contexts/map-0.2.jsonld" + }, + "@type": { + "const": "MailAction" + }, + "@id": { + "$ref": "#/$defs/uuidUrn" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "describedAt": { + "$ref": "#/$defs/dateTime" + }, + "expiresAt": { + "$ref": "#/$defs/dateTime" + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/httpsIdentifier" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "authority": { + "enum": ["credential", "possession"] + }, + "onBehalfOf": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + } + }, + "required": ["id", "name"] + }, + "resource": { + "$ref": "#/$defs/httpsTarget" + }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "$ref": "#/$defs/httpsTarget" + }, + "resultUrlTemplate": { + "$ref": "#/$defs/urlTemplate" + }, + "resultRetentionSeconds": { + "type": "integer", + "minimum": 300, + "maximum": 31536000 + } + }, + "required": ["url", "resultUrlTemplate", "resultRetentionSeconds"] + }, + "humanUrl": { + "$ref": "#/$defs/httpsIdentifier" + } + }, + "required": ["id", "name", "authority", "execution", "humanUrl"] + }, + "recipient": { + "$ref": "#/$defs/address" + }, + "target": { + "$ref": "#/$defs/target" + }, + "details": { + "type": "object", + "maxProperties": 100, + "propertyNames": { + "$ref": "#/$defs/detailsKey" + }, + "additionalProperties": { + "$ref": "#/$defs/detailsValue" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/operation" + }, + "minItems": 1, + "maxItems": 100 + } + }, + "required": [ + "@context", + "@type", + "@id", + "profile", + "type", + "describedAt", + "expiresAt", + "service", + "target", + "operations" + ], + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "service": { + "type": "object", + "properties": { + "authority": { + "const": "credential" + } + } + } + } + }, + "then": { + "type": "object", + "properties": { + "service": { + "type": "object", + "required": ["resource"] + } + }, + "not": { + "required": ["recipient"] + } + }, + "else": { + "type": "object", + "properties": { + "service": { + "type": "object", + "properties": { + "resource": false, + "onBehalfOf": false + } + } + }, + "required": ["recipient"] + } + } + ] + }, + "request": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapRequest" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "descriptionDigest": { + "$ref": "#/$defs/digest" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "operation": { + "$ref": "#/$defs/token" + }, + "input": { + "type": "object", + "maxProperties": 100 + } + }, + "required": [ + "kind", + "profile", + "requestId", + "interactionId", + "descriptionDigest", + "type", + "operation", + "input" + ] + }, + "result": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapResult" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "descriptionDigest": { + "$ref": "#/$defs/digest" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "operation": { + "$ref": "#/$defs/token" + }, + "state": { + "enum": ["accepted", "completed", "failed", "pending", "approval-required"] + }, + "target": { + "$ref": "#/$defs/target" + }, + "recordedAt": { + "$ref": "#/$defs/dateTime" + }, + "resultUrl": { + "$ref": "#/$defs/httpsIdentifier" + }, + "actor": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "approvalUrl": { + "$ref": "#/$defs/httpsIdentifier" + }, + "reason": { + "$ref": "#/$defs/token" + }, + "output": { + "type": "object", + "maxProperties": 100 + } + }, + "required": [ + "kind", + "profile", + "requestId", + "interactionId", + "descriptionDigest", + "type", + "operation", + "state", + "target", + "recordedAt", + "resultUrl", + "output" + ], + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "state": { + "const": "approval-required" + } + } + }, + "then": { + "type": "object", + "required": ["approvalUrl"] + }, + "else": { + "type": "object", + "not": { + "required": ["approvalUrl"] + } + } + }, + { + "if": { + "type": "object", + "properties": { + "state": { + "const": "failed" + } + } + }, + "then": { + "type": "object", + "required": ["reason"] + }, + "else": { + "type": "object", + "not": { + "required": ["reason"] + } + } + } + ] + }, + "problem": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "https://mailschema.org/problems/refused", + "https://mailschema.org/problems/authentication-required", + "https://mailschema.org/problems/result-not-found", + "https://mailschema.org/problems/stale-target", + "https://mailschema.org/problems/expired-interaction", + "https://mailschema.org/problems/invalid-request", + "https://mailschema.org/problems/unsupported-type", + "https://mailschema.org/problems/unsupported-operation", + "https://mailschema.org/problems/idempotency-conflict", + "https://mailschema.org/problems/request-in-progress", + "https://mailschema.org/problems/already-decided" + ] + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "status": { + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "detail": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "instance": { + "$ref": "#/$defs/identifier" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "code": { + "enum": [ + "refused", + "result-not-found", + "stale-target", + "expired-interaction", + "invalid-request", + "unsupported-type", + "unsupported-operation", + "idempotency-conflict", + "request-in-progress", + "already-decided" + ] + }, + "target": { + "$ref": "#/$defs/target" + }, + "errors": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "detail": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "pointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "maxLength": 1000 + } + }, + "required": ["detail", "pointer"] + } + } + }, + "required": ["type", "title", "status", "detail"], + "dependentRequired": { + "instance": ["profile", "requestId", "code"], + "profile": ["instance", "requestId", "code"], + "requestId": ["instance", "profile", "code"], + "interactionId": ["instance", "profile", "requestId", "code"], + "code": ["instance", "profile", "requestId"] + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "https://mailschema.org/problems/result-not-found", + "https://mailschema.org/problems/stale-target", + "https://mailschema.org/problems/expired-interaction", + "https://mailschema.org/problems/unsupported-type", + "https://mailschema.org/problems/unsupported-operation", + "https://mailschema.org/problems/idempotency-conflict", + "https://mailschema.org/problems/request-in-progress", + "https://mailschema.org/problems/already-decided" + ] + } + }, + "required": ["type"] + }, + "then": { + "required": ["code"] + } + }, + { + "if": { + "required": ["errors"] + }, + "then": { + "properties": { + "code": { + "const": "invalid-request" + } + }, + "required": ["code"] + } + }, + { + "if": { + "properties": { + "code": { + "not": { + "const": "result-not-found" + } + } + }, + "required": ["code"] + }, + "then": { + "required": ["interactionId"] + } + }, + { + "if": { + "properties": { + "code": { + "const": "invalid-request" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/invalid-request" + }, + "status": { + "const": 400 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "refused" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/refused" + }, + "status": { + "const": 403 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "result-not-found" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/result-not-found" + }, + "status": { + "const": 404 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "stale-target" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/stale-target" + }, + "status": { + "const": 409 + } + }, + "required": ["target"] + } + }, + { + "if": { + "properties": { + "code": { + "const": "idempotency-conflict" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/idempotency-conflict" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "request-in-progress" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/request-in-progress" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "already-decided" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/already-decided" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "expired-interaction" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/expired-interaction" + }, + "status": { + "const": 410 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "unsupported-type" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-type" + }, + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "unsupported-operation" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-operation" + }, + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/invalid-request" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 400 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/authentication-required" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 401 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/refused" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 403 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/result-not-found" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 404 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/stale-target" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/idempotency-conflict" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/request-in-progress" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/already-decided" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/expired-interaction" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 410 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-type" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-operation" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 422 + } + } + } + } + ] + }, + "detailsKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$" + }, + "detailsValue": { + "anyOf": [ + { + "type": "string", + "maxLength": 12000 + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/$defs/detailsValue" + } + }, + { + "type": "object", + "maxProperties": 100, + "propertyNames": { + "$ref": "#/$defs/detailsKey" + }, + "additionalProperties": { + "$ref": "#/$defs/detailsValue" + } + } + ] + } + } +} diff --git a/dist/map.d.ts b/dist/map.d.ts index 2832336..595326a 100644 --- a/dist/map.d.ts +++ b/dist/map.d.ts @@ -1,11 +1,10 @@ -export declare function mapErrors(value: unknown): string[]; -export declare function assertMapDocument(value: unknown): void; -export declare function contentReviewRequestErrors(value: unknown): string[]; -export declare function assertContentReviewRequest(value: unknown): void; -export declare function contentReview01RequestErrors(value: unknown): string[]; -export declare function assertContentReview01Request(value: unknown): void; +/** The MAP profile whose core artifacts this package carries. */ +export declare const MAP_PROFILE = "https://mailschema.org/profiles/map/0.2"; +/** A fresh copy of the MAP 0.2 core schema, as the profile record binds it by SHA-256. */ export declare function getMapSchema(): Record; -export declare function getContentReviewSchema(): Record; -export declare function getContentReviewContract(): Record; -export declare function getContentReview01Schema(): Record; -export declare function getContentReview01Contract(): Record; +/** A fresh copy of the MAP 0.2 JSON-LD context. */ +export declare function getMapContext(): Record; +/** A fresh copy of the type contract format every MAP 0.2 contract follows. */ +export declare function getContractFormatSchema(): Record; +/** A fresh copy of the form fields block contracts pin. */ +export declare function getFormsSchema(): Record; diff --git a/dist/map.js b/dist/map.js index ce9ac83..f2e9ee9 100644 --- a/dist/map.js +++ b/dist/map.js @@ -1,57 +1,20 @@ -import Ajv2020 from 'ajv/dist/2020.js'; -import addFormats from 'ajv-formats'; -import mapSchema from './map-0.1.schema.json' with { type: 'json' }; -import contentReview01Schema from './content-review-0.1.schema.json' with { type: 'json' }; -import contentReview01Contract from './content-review-0.1.contract.json' with { type: 'json' }; -import contentReview02Schema from './content-review-0.2.schema.json' with { type: 'json' }; -import contentReview02Contract from './content-review-0.2.contract.json' with { type: 'json' }; -const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false }); -addFormats(ajv); -ajv.addSchema(mapSchema); -const mapDocument = ajv.getSchema(mapSchema.$id); -const contentReview01Request = ajv.compile(contentReview01Schema); -const contentReview02Request = ajv.compile(contentReview02Schema); -function errors(validate, value) { - if (validate(value)) - return []; - return (validate.errors || []).map((error) => `${error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`); -} -export function mapErrors(value) { - return errors(mapDocument, value); -} -export function assertMapDocument(value) { - const found = mapErrors(value); - if (found.length) - throw new Error(`Invalid MAP 0.1 document:\n${found.join('\n')}`); -} -export function contentReviewRequestErrors(value) { - return errors(contentReview02Request, value); -} -export function assertContentReviewRequest(value) { - const found = contentReviewRequestErrors(value); - if (found.length) - throw new Error(`Invalid Content Review 0.2 request:\n${found.join('\n')}`); -} -export function contentReview01RequestErrors(value) { - return errors(contentReview01Request, value); -} -export function assertContentReview01Request(value) { - const found = contentReview01RequestErrors(value); - if (found.length) - throw new Error(`Invalid Content Review 0.1 request:\n${found.join('\n')}`); -} +import { readFileSync } from 'node:fs'; +/** The MAP profile whose core artifacts this package carries. */ +export const MAP_PROFILE = 'https://mailschema.org/profiles/map/0.2'; +const artifact = (name) => JSON.parse(readFileSync(new URL(`./${name}`, import.meta.url), 'utf8')); +/** A fresh copy of the MAP 0.2 core schema, as the profile record binds it by SHA-256. */ export function getMapSchema() { - return structuredClone(mapSchema); -} -export function getContentReviewSchema() { - return structuredClone(contentReview02Schema); + return artifact('map-0.2.schema.json'); } -export function getContentReviewContract() { - return structuredClone(contentReview02Contract); +/** A fresh copy of the MAP 0.2 JSON-LD context. */ +export function getMapContext() { + return artifact('map-0.2.jsonld'); } -export function getContentReview01Schema() { - return structuredClone(contentReview01Schema); +/** A fresh copy of the type contract format every MAP 0.2 contract follows. */ +export function getContractFormatSchema() { + return artifact('type-contract-0.2.schema.json'); } -export function getContentReview01Contract() { - return structuredClone(contentReview01Contract); +/** A fresh copy of the form fields block contracts pin. */ +export function getFormsSchema() { + return artifact('forms-0.1.schema.json'); } diff --git a/dist/type-contract-0.2.schema.json b/dist/type-contract-0.2.schema.json new file mode 100644 index 0000000..1fe14b2 --- /dev/null +++ b/dist/type-contract-0.2.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/type-contract-0.2.schema.json", + "title": "MailSchema type contract for MAP 0.2", + "description": "A versioned interaction contract: the details, inputs, results, authority, repeatability and consequences of each operation, with every schema it depends on pinned by digest.", + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapTypeContract" + }, + "id": { + "type": "string", + "pattern": "^https://mailschema\\.org/types/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 2048 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", + "maxLength": 80 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.2" + }, + "target": { + "$ref": "#/$defs/prose" + }, + "dependencies": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/$defs/schemaReference" + } + }, + "detailsSchema": { + "type": "object" + }, + "requestSchema": { + "$ref": "#/$defs/schemaReference" + }, + "operations": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/token" + }, + "effect": { + "$ref": "#/$defs/prose" + }, + "authority": { + "type": "array", + "items": { + "enum": ["credential", "possession"] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "consequences": { + "type": "array", + "items": { + "enum": [ + "refusal", + "protection", + "record", + "disclosure", + "commitment", + "authorization", + "assertion" + ] + }, + "minItems": 1, + "maxItems": 7, + "uniqueItems": true + }, + "repeatable": { + "type": "boolean" + }, + "fieldBindings": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "input": { + "$ref": "#/$defs/pointer" + }, + "fields": { + "$ref": "#/$defs/pointer" + } + }, + "required": ["input", "fields"] + } + }, + "results": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "enum": ["accepted", "completed", "failed", "pending", "approval-required"] + }, + "reasons": { + "type": "array", + "items": { + "$ref": "#/$defs/token" + }, + "minItems": 1, + "maxItems": 20, + "uniqueItems": true + }, + "outputSchema": { + "type": "object" + } + }, + "required": ["state", "outputSchema"] + } + } + }, + "required": ["id", "effect", "authority", "consequences", "results"] + } + } + }, + "required": [ + "kind", + "id", + "version", + "profile", + "target", + "dependencies", + "requestSchema", + "operations" + ], + "$defs": { + "prose": { + "type": "string", + "minLength": 1, + "maxLength": 12000, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "token": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 100 + }, + "pointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)+$", + "maxLength": 1000 + }, + "schemaReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "pattern": "^https://mailschema\\.org/schemas/[a-z0-9.-]+\\.json$", + "maxLength": 2048 + }, + "canonicalDigest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + } + }, + "required": ["url", "canonicalDigest"] + } + } +} diff --git a/package-lock.json b/package-lock.json index e148afd..21a402b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mailschema", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mailschema", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "ajv": "8.20.0", @@ -16,12 +16,23 @@ "mailschema": "bin/mailschema.js" }, "devDependencies": { + "@types/node": "24.13.6", "typescript": "5.9.3" }, "engines": { "node": ">=22" } }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -105,6 +116,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 08d1d71..baf1f0c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "mailschema", - "version": "0.1.4", - "description": "Schemas and validation tools for Mail Action Protocol and the MailSchema Registry", + "version": "0.1.5", + "description": "Mail Action Protocol 0.2 core artifacts and MailSchema Registry validation", "type": "module", "license": "MIT", "author": "MailSchema contributors", - "homepage": "https://mailschema.org/tools/", + "homepage": "https://mailschema.org/tools", "repository": { "type": "git", "url": "git+https://github.com/mailschema/javascript.git" @@ -22,11 +22,10 @@ "import": "./dist/index.js" }, "./contribution.schema.json": "./dist/contribution.schema.json", - "./map-0.1.schema.json": "./dist/map-0.1.schema.json", - "./content-review-0.1.schema.json": "./dist/content-review-0.1.schema.json", - "./content-review-0.1.contract.json": "./dist/content-review-0.1.contract.json", - "./content-review-0.2.schema.json": "./dist/content-review-0.2.schema.json", - "./content-review-0.2.contract.json": "./dist/content-review-0.2.contract.json", + "./map-0.2.schema.json": "./dist/map-0.2.schema.json", + "./map-0.2.jsonld": "./dist/map-0.2.jsonld", + "./type-contract-0.2.schema.json": "./dist/type-contract-0.2.schema.json", + "./forms-0.1.schema.json": "./dist/forms-0.1.schema.json", "./package.json": "./package.json" }, "types": "./dist/index.d.ts", @@ -48,6 +47,7 @@ "ajv-formats": "3.0.1" }, "devDependencies": { + "@types/node": "24.13.6", "typescript": "5.9.3" }, "keywords": [ diff --git a/src/content-review-0.1.contract.json b/src/content-review-0.1.contract.json deleted file mode 100644 index 922baef..0000000 --- a/src/content-review-0.1.contract.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/content-review", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.1", - "target": "Service-owned content at the exact revision and digest named in the request.", - "requestSchema": { - "url": "https://mailschema.org/schemas/content-review-0.1.schema.json", - "canonicalDigest": "sha-256:cdad1149ab193d1bf71cb9463e3c4b7331ed56b0123e225adf68f70a2022b29d" - }, - "operations": [ - { - "id": "request-changes", - "effect": "Record review feedback on the named revision.", - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedbackRecorded": { "const": true } - }, - "required": ["feedbackRecorded"] - } - } - ] - }, - { - "id": "approve", - "effect": "Record approval of the named revision.", - "results": [ - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "approvalUrl": { "type": "string", "format": "uri", "pattern": "^https://" } - }, - "required": ["approvalUrl"] - } - }, - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { "const": "approved" } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/src/content-review-0.1.schema.json b/src/content-review-0.1.schema.json deleted file mode 100644 index 57fc90f..0000000 --- a/src/content-review-0.1.schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/content-review-0.1.schema.json", - "title": "Content Review 0.1 MAP request binding", - "allOf": [ - { "$ref": "https://mailschema.org/schemas/map-0.1.schema.json#/$defs/request" }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { "const": "https://mailschema.org/types/content-review" }, - "version": { "const": "0.1" } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { "const": "request-changes" }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedback": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["feedback"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { "const": "approve" }, - "input": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - } - } - } - ] - } - ] -} diff --git a/src/content-review-0.2.contract.json b/src/content-review-0.2.contract.json deleted file mode 100644 index 7dec56a..0000000 --- a/src/content-review-0.2.contract.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/content-review", - "version": "0.2", - "profile": "https://mailschema.org/profiles/map/0.1", - "target": "Service-owned content at the exact revision and digest named in the request.", - "requestSchema": { - "url": "https://mailschema.org/schemas/content-review-0.2.schema.json", - "canonicalDigest": "sha-256:43a01e620e0730e47755694bdf5d086f8d0a1d3d8328fb0c7d2a4b70c6e62e25" - }, - "operations": [ - { - "id": "request-changes", - "effect": "Record review feedback on the named revision.", - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedbackRecorded": { - "const": true - } - }, - "required": [ - "feedbackRecorded" - ] - } - } - ] - }, - { - "id": "approve", - "effect": "Record approval of the named revision, or record why a proposed approval ended without approval.", - "results": [ - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "approvalUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://" - } - }, - "required": [ - "approvalUrl" - ] - } - }, - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "approved" - } - }, - "required": [ - "decision" - ] - } - }, - { - "state": "failed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "enum": [ - "declined", - "stale-target", - "expired" - ] - } - }, - "required": [ - "reason" - ] - } - } - ] - } - ] -} diff --git a/src/content-review-0.2.schema.json b/src/content-review-0.2.schema.json deleted file mode 100644 index 88b5be7..0000000 --- a/src/content-review-0.2.schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/content-review-0.2.schema.json", - "title": "Content Review 0.2 MAP request binding", - "allOf": [ - { "$ref": "https://mailschema.org/schemas/map-0.1.schema.json#/$defs/request" }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { "const": "https://mailschema.org/types/content-review" }, - "version": { "const": "0.2" } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { "const": "request-changes" }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedback": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["feedback"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { "const": "approve" }, - "input": { - "type": "object", - "additionalProperties": false, - "maxProperties": 0 - } - } - } - ] - } - ] -} diff --git a/src/forms-0.1.schema.json b/src/forms-0.1.schema.json new file mode 100644 index 0000000..88f680b --- /dev/null +++ b/src/forms-0.1.schema.json @@ -0,0 +1,329 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/forms-0.1.schema.json", + "title": "MailSchema form fields 0.1", + "description": "Shared, immutable blocks for types that collect values: a closed subset of JSON Schema based on Model Context Protocol elicitation, with HTML autofill field names.", + "$defs": { + "fieldName": { + "type": "string", + "pattern": "^[a-z][A-Za-z0-9]{0,63}$" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "autocomplete": { + "type": "string", + "maxLength": 200, + "pattern": "^(?:section-[a-z0-9-]+ )?(?:(?:shipping|billing) )?(?:(?:home|work|mobile|fax|pager) )?[a-z][a-z0-9-]*$", + "not": { + "pattern": "(?:^| )(?:current-password|new-password|one-time-code|cc-[a-z-]+)$" + } + }, + "choice": { + "type": "object", + "additionalProperties": false, + "properties": { + "const": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + } + }, + "required": ["const", "title"] + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/$defs/choice" + }, + "minItems": 1, + "maxItems": 100 + }, + "textField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "string" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "minLength": { + "type": "integer", + "minimum": 0, + "maximum": 12000 + }, + "maxLength": { + "type": "integer", + "minimum": 1, + "maximum": 12000 + }, + "format": { + "enum": ["email", "uri", "date", "date-time"] + }, + "default": { + "type": "string", + "maxLength": 12000 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title"] + }, + "numberField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": ["number", "integer"] + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "minimum": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "maximum": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "default": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title"] + }, + "booleanField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "boolean" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "default": { + "type": "boolean" + } + }, + "required": ["type", "title"] + }, + "choiceField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "string" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "oneOf": { + "$ref": "#/$defs/choices" + }, + "default": { + "type": "string", + "maxLength": 200 + }, + "autocomplete": { + "$ref": "#/$defs/autocomplete" + } + }, + "required": ["type", "title", "oneOf"] + }, + "choicesField": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "array" + }, + "title": { + "$ref": "#/$defs/text" + }, + "description": { + "$ref": "#/$defs/text" + }, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "anyOf": { + "$ref": "#/$defs/choices" + } + }, + "required": ["anyOf"] + }, + "minItems": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "maxItems": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "uniqueItems": { + "const": true + }, + "default": { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + "required": ["type", "title", "items", "uniqueItems"] + }, + "fields": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "object" + }, + "properties": { + "type": "object", + "minProperties": 1, + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/$defs/textField" + }, + { + "$ref": "#/$defs/numberField" + }, + { + "$ref": "#/$defs/booleanField" + }, + { + "$ref": "#/$defs/choiceField" + }, + { + "$ref": "#/$defs/choicesField" + } + ] + } + }, + "required": { + "type": "array", + "items": { + "$ref": "#/$defs/fieldName" + }, + "uniqueItems": true, + "maxItems": 50 + } + }, + "required": ["type", "properties"] + }, + "choiceFields": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "const": "object" + }, + "properties": { + "type": "object", + "minProperties": 1, + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/$defs/booleanField" + }, + { + "$ref": "#/$defs/choiceField" + }, + { + "$ref": "#/$defs/choicesField" + } + ] + } + }, + "required": { + "type": "array", + "items": { + "$ref": "#/$defs/fieldName" + }, + "uniqueItems": true, + "maxItems": 50 + } + }, + "required": ["type", "properties"] + }, + "fieldValues": { + "type": "object", + "maxProperties": 50, + "propertyNames": { + "$ref": "#/$defs/fieldName" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 12000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 100, + "uniqueItems": true + } + ] + } + } + } +} diff --git a/src/index.ts b/src/index.ts index a1a863e..ac8fb00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -/** Schemas and validation tools for MAP documents and MailSchema Registry contributions. */ +/** The MAP 0.2 core artifacts and validation tools for MailSchema Registry contributions. */ import schema from './contribution.schema.json' with { type: 'json' }; export { @@ -11,17 +11,11 @@ export type { CatalogView } from './validation.js'; export { typeStages } from './model.js'; export type { Party, TypeDefinition, TypeRecord, Contribution, Implementation } from './model.js'; export { - mapErrors, - assertMapDocument, - contentReviewRequestErrors, - assertContentReviewRequest, - contentReview01RequestErrors, - assertContentReview01Request, + MAP_PROFILE, getMapSchema, - getContentReviewSchema, - getContentReviewContract, - getContentReview01Schema, - getContentReview01Contract, + getMapContext, + getContractFormatSchema, + getFormsSchema, } from './map.js'; /** Return an independent copy of the contribution JSON Schema (Draft 2020-12). */ diff --git a/src/map-0.1.schema.json b/src/map-0.1.schema.json deleted file mode 100644 index 352cd5d..0000000 --- a/src/map-0.1.schema.json +++ /dev/null @@ -1,411 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/map-0.1.schema.json", - "title": "Mail Action Protocol 0.1", - "description": "Description, request, result and problem documents for the MAP 0.1 authenticated HTTPS profile.", - "oneOf": [ - { "$ref": "#/$defs/description" }, - { "$ref": "#/$defs/request" }, - { "$ref": "#/$defs/result" }, - { "$ref": "#/$defs/problem" } - ], - "$defs": { - "profile": { - "const": "https://mailschema.org/profiles/map/0.1" - }, - "identifier": { - "type": "string", - "format": "uri", - "maxLength": 2048 - }, - "httpsIdentifier": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "uuidUrn": { - "type": "string", - "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - }, - "digest": { - "type": "string", - "pattern": "^sha-256:[a-f0-9]{64}$" - }, - "contractDigest": { - "type": "string", - "pattern": "^sha-256:[a-f0-9]{64}$" - }, - "typeReference": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - "contractDigest": { "$ref": "#/$defs/contractDigest" } - }, - "required": ["id", "version", "contractDigest"] - }, - "target": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "revision": { "type": "string", "minLength": 1, "maxLength": 240 }, - "title": { "type": "string", "minLength": 1, "maxLength": 500 }, - "digest": { "$ref": "#/$defs/digest" } - }, - "required": ["id", "revision", "digest"] - }, - "operation": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { "type": "string", "minLength": 1, "maxLength": 240 }, - "description": { "type": "string", "minLength": 1, "maxLength": 2000 }, - "inputSchema": { "$ref": "#/$defs/identifier" } - }, - "required": ["id", "name", "description", "inputSchema"] - }, - "description": { - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "const": "https://mailschema.org/contexts/map-0.1.jsonld" - }, - "@type": { "const": "MailAction" }, - "@id": { "$ref": "#/$defs/uuidUrn" }, - "profile": { "$ref": "#/$defs/profile" }, - "type": { "$ref": "#/$defs/typeReference" }, - "describedAt": { "type": "string", "format": "date-time" }, - "expiresAt": { "type": "string", "format": "date-time" }, - "service": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "name": { "type": "string", "minLength": 1, "maxLength": 240 }, - "execution": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { "$ref": "#/$defs/httpsIdentifier" }, - "method": { "const": "POST" }, - "requestMediaType": { "const": "application/json" }, - "resultMediaType": { "const": "application/json" }, - "resultUrlTemplate": { - "type": "string", - "format": "uri-template", - "pattern": "^https://.*\\{requestId\\}.*$", - "maxLength": 2048 - }, - "resultRetentionSeconds": { - "type": "integer", - "minimum": 300, - "maximum": 31536000 - } - }, - "required": [ - "url", - "method", - "requestMediaType", - "resultMediaType", - "resultUrlTemplate", - "resultRetentionSeconds" - ] - }, - "humanUrl": { "$ref": "#/$defs/httpsIdentifier" }, - "authorization": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "service-configured" }, - "schemes": { - "type": "array", - "items": { "enum": ["oauth2", "bearer", "session"] }, - "minItems": 1, - "uniqueItems": true - }, - "audience": { "type": "string", "minLength": 1, "maxLength": 500 } - }, - "required": ["kind", "schemes"] - } - }, - "required": ["id", "name", "execution", "humanUrl", "authorization"] - }, - "target": { "$ref": "#/$defs/target" }, - "operations": { - "type": "array", - "items": { "$ref": "#/$defs/operation" }, - "minItems": 1, - "maxItems": 100 - } - }, - "required": [ - "@context", - "@type", - "@id", - "profile", - "type", - "describedAt", - "expiresAt", - "service", - "target", - "operations" - ] - }, - "request": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "MapRequest" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "type": { "$ref": "#/$defs/typeReference" }, - "operation": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "target": { "$ref": "#/$defs/target" }, - "input": { "type": "object", "maxProperties": 100 } - }, - "required": [ - "kind", - "profile", - "requestId", - "interactionId", - "type", - "operation", - "target", - "input" - ] - }, - "result": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { "const": "MapResult" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "type": { "$ref": "#/$defs/typeReference" }, - "operation": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "state": { - "enum": ["accepted", "completed", "failed", "pending", "approval-required"] - }, - "target": { "$ref": "#/$defs/target" }, - "recordedAt": { "type": "string", "format": "date-time" }, - "resultUrl": { "$ref": "#/$defs/httpsIdentifier" }, - "output": { "type": "object", "maxProperties": 100 } - }, - "required": [ - "kind", - "profile", - "requestId", - "interactionId", - "type", - "operation", - "state", - "target", - "recordedAt", - "resultUrl", - "output" - ] - }, - "problem": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "enum": [ - "https://mailschema.org/problems/refused", - "https://mailschema.org/problems/authentication-required", - "https://mailschema.org/problems/result-not-found", - "https://mailschema.org/problems/stale-target", - "https://mailschema.org/problems/expired-interaction", - "https://mailschema.org/problems/invalid-request", - "https://mailschema.org/problems/unsupported-profile", - "https://mailschema.org/problems/unsupported-type", - "https://mailschema.org/problems/unsupported-operation", - "https://mailschema.org/problems/idempotency-conflict", - "https://mailschema.org/problems/request-in-progress" - ] - }, - "title": { "type": "string", "minLength": 1, "maxLength": 240 }, - "status": { "type": "integer", "minimum": 400, "maximum": 599 }, - "detail": { "type": "string", "minLength": 1, "maxLength": 4000 }, - "instance": { "$ref": "#/$defs/identifier" }, - "profile": { "$ref": "#/$defs/profile" }, - "requestId": { "$ref": "#/$defs/uuidUrn" }, - "interactionId": { "$ref": "#/$defs/uuidUrn" }, - "code": { - "enum": [ - "refused", - "authentication-required", - "result-not-found", - "stale-target", - "expired-interaction", - "invalid-request", - "unsupported-profile", - "unsupported-type", - "unsupported-operation", - "idempotency-conflict", - "request-in-progress" - ] - }, - "target": { "$ref": "#/$defs/target" } - }, - "required": ["type", "title", "status", "detail"], - "dependentRequired": { - "instance": ["profile", "requestId", "code"], - "profile": ["instance", "requestId", "code"], - "requestId": ["instance", "profile", "code"], - "interactionId": ["instance", "profile", "requestId", "code"], - "code": ["instance", "profile", "requestId"] - }, - "allOf": [ - { - "if": { - "properties": { "code": { "not": { "const": "result-not-found" } } }, - "required": ["code"] - }, - "then": { "required": ["interactionId"] } - }, - { - "if": { "properties": { "code": { "const": "invalid-request" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/invalid-request" }, - "status": { "const": 400 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "authentication-required" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/authentication-required" }, - "status": { "const": 401 } - } - } - }, - { - "if": { "properties": { "code": { "const": "refused" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/refused" }, - "status": { "const": 403 } - } - } - }, - { - "if": { "properties": { "code": { "const": "result-not-found" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/result-not-found" }, - "status": { "const": 404 } - } - } - }, - { - "if": { "properties": { "code": { "const": "stale-target" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/stale-target" }, - "status": { "const": 409 } - }, - "required": ["target"] - } - }, - { - "if": { - "properties": { "code": { "const": "idempotency-conflict" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/idempotency-conflict" }, - "status": { "const": 409 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "request-in-progress" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/request-in-progress" }, - "status": { "const": 409 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "expired-interaction" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/expired-interaction" }, - "status": { "const": 410 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "unsupported-profile" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-profile" }, - "status": { "const": 422 } - } - } - }, - { - "if": { "properties": { "code": { "const": "unsupported-type" } }, "required": ["code"] }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-type" }, - "status": { "const": 422 } - } - } - }, - { - "if": { - "properties": { "code": { "const": "unsupported-operation" } }, - "required": ["code"] - }, - "then": { - "properties": { - "type": { "const": "https://mailschema.org/problems/unsupported-operation" }, - "status": { "const": 422 } - } - } - } - ] - } - } -} diff --git a/src/map-0.2.jsonld b/src/map-0.2.jsonld new file mode 100644 index 0000000..c97aca3 --- /dev/null +++ b/src/map-0.2.jsonld @@ -0,0 +1,91 @@ +{ + "@context": { + "@version": 1.1, + "map": "https://mailschema.org/ns/map#", + "schema": "https://schema.org/", + "MailAction": "map:MailAction", + "profile": { + "@id": "map:profile", + "@type": "@id" + }, + "type": { + "@id": "map:interactionType", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "version": "schema:version", + "contractDigest": "map:contractDigest", + "describedAt": { + "@id": "schema:dateCreated", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expiresAt": { + "@id": "schema:expires", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "service": { + "@id": "schema:provider", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "name": "schema:name", + "authority": "map:authority", + "onBehalfOf": { + "@id": "map:onBehalfOf", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "resource": { + "@id": "map:resource", + "@type": "@id" + }, + "execution": "map:execution", + "url": { + "@id": "schema:url", + "@type": "@id" + }, + "resultUrlTemplate": "map:resultUrlTemplate", + "resultRetentionSeconds": "map:resultRetentionSeconds", + "humanUrl": { + "@id": "map:humanUrl", + "@type": "@id" + }, + "target": { + "@id": "schema:object", + "@context": { + "id": { + "@id": "map:id", + "@type": "@id" + } + } + }, + "recipient": "schema:recipient", + "details": { + "@id": "map:details", + "@type": "@json" + }, + "revision": "schema:version", + "title": "schema:name", + "digest": "map:digest", + "operations": { + "@id": "schema:potentialAction", + "@container": "@set", + "@context": { + "id": "map:operationId" + } + }, + "description": "schema:description" + } +} diff --git a/src/map-0.2.schema.json b/src/map-0.2.schema.json new file mode 100644 index 0000000..a87b566 --- /dev/null +++ b/src/map-0.2.schema.json @@ -0,0 +1,1033 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/map-0.2.schema.json", + "title": "Mail Action Protocol 0.2", + "description": "Description, request, result and problem documents for MAP 0.2: a type-agnostic core whose types are defined by digest-bound contracts, with credential or possession authority over HTTPS.", + "oneOf": [ + { + "$ref": "#/$defs/description" + }, + { + "$ref": "#/$defs/request" + }, + { + "$ref": "#/$defs/result" + }, + { + "$ref": "#/$defs/problem" + } + ], + "$defs": { + "profile": { + "const": "https://mailschema.org/profiles/map/0.2" + }, + "nonBlank": { + "type": "string", + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "dateTime": { + "type": "string", + "pattern": "^(?:[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]{1,3})?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$" + }, + "date": { + "type": "string", + "pattern": "^(?:[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)$" + }, + "identifier": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+$", + "maxLength": 2048 + }, + "httpsIdentifier": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?#](?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*)?$", + "maxLength": 2048 + }, + "httpsTarget": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?](?:[A-Za-z0-9._~:/?@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*)?$", + "maxLength": 2048 + }, + "httpsOrigin": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$", + "maxLength": 300 + }, + "urlTemplate": { + "type": "string", + "pattern": "^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?::(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?[/?](?:[A-Za-z0-9._~:/?@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*\\{requestId\\}(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})*$", + "maxLength": 2048 + }, + "address": { + "type": "string", + "pattern": "^(?:[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]|[^\\u0000-\\u00a0\\u1680\\u2000-\\u200f\\u2028-\\u202f\\u205f-\\u206f\\u3000\\ufeff])+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", + "maxLength": 320 + }, + "uuidUrn": { + "type": "string", + "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + }, + "contractDigest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + }, + "typeReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", + "maxLength": 80 + }, + "contractDigest": { + "$ref": "#/$defs/contractDigest" + } + }, + "required": ["id", "version", "contractDigest"] + }, + "token": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 100 + }, + "target": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "digest": { + "$ref": "#/$defs/digest" + } + }, + "required": ["id", "revision", "digest"] + }, + "operation": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/token" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + }, + "required": ["id", "name", "description"] + }, + "description": { + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "const": "https://mailschema.org/contexts/map-0.2.jsonld" + }, + "@type": { + "const": "MailAction" + }, + "@id": { + "$ref": "#/$defs/uuidUrn" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "describedAt": { + "$ref": "#/$defs/dateTime" + }, + "expiresAt": { + "$ref": "#/$defs/dateTime" + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/httpsIdentifier" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "authority": { + "enum": ["credential", "possession"] + }, + "onBehalfOf": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + } + }, + "required": ["id", "name"] + }, + "resource": { + "$ref": "#/$defs/httpsTarget" + }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "$ref": "#/$defs/httpsTarget" + }, + "resultUrlTemplate": { + "$ref": "#/$defs/urlTemplate" + }, + "resultRetentionSeconds": { + "type": "integer", + "minimum": 300, + "maximum": 31536000 + } + }, + "required": ["url", "resultUrlTemplate", "resultRetentionSeconds"] + }, + "humanUrl": { + "$ref": "#/$defs/httpsIdentifier" + } + }, + "required": ["id", "name", "authority", "execution", "humanUrl"] + }, + "recipient": { + "$ref": "#/$defs/address" + }, + "target": { + "$ref": "#/$defs/target" + }, + "details": { + "type": "object", + "maxProperties": 100, + "propertyNames": { + "$ref": "#/$defs/detailsKey" + }, + "additionalProperties": { + "$ref": "#/$defs/detailsValue" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/operation" + }, + "minItems": 1, + "maxItems": 100 + } + }, + "required": [ + "@context", + "@type", + "@id", + "profile", + "type", + "describedAt", + "expiresAt", + "service", + "target", + "operations" + ], + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "service": { + "type": "object", + "properties": { + "authority": { + "const": "credential" + } + } + } + } + }, + "then": { + "type": "object", + "properties": { + "service": { + "type": "object", + "required": ["resource"] + } + }, + "not": { + "required": ["recipient"] + } + }, + "else": { + "type": "object", + "properties": { + "service": { + "type": "object", + "properties": { + "resource": false, + "onBehalfOf": false + } + } + }, + "required": ["recipient"] + } + } + ] + }, + "request": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapRequest" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "descriptionDigest": { + "$ref": "#/$defs/digest" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "operation": { + "$ref": "#/$defs/token" + }, + "input": { + "type": "object", + "maxProperties": 100 + } + }, + "required": [ + "kind", + "profile", + "requestId", + "interactionId", + "descriptionDigest", + "type", + "operation", + "input" + ] + }, + "result": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapResult" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "descriptionDigest": { + "$ref": "#/$defs/digest" + }, + "type": { + "$ref": "#/$defs/typeReference" + }, + "operation": { + "$ref": "#/$defs/token" + }, + "state": { + "enum": ["accepted", "completed", "failed", "pending", "approval-required"] + }, + "target": { + "$ref": "#/$defs/target" + }, + "recordedAt": { + "$ref": "#/$defs/dateTime" + }, + "resultUrl": { + "$ref": "#/$defs/httpsIdentifier" + }, + "actor": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "approvalUrl": { + "$ref": "#/$defs/httpsIdentifier" + }, + "reason": { + "$ref": "#/$defs/token" + }, + "output": { + "type": "object", + "maxProperties": 100 + } + }, + "required": [ + "kind", + "profile", + "requestId", + "interactionId", + "descriptionDigest", + "type", + "operation", + "state", + "target", + "recordedAt", + "resultUrl", + "output" + ], + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "state": { + "const": "approval-required" + } + } + }, + "then": { + "type": "object", + "required": ["approvalUrl"] + }, + "else": { + "type": "object", + "not": { + "required": ["approvalUrl"] + } + } + }, + { + "if": { + "type": "object", + "properties": { + "state": { + "const": "failed" + } + } + }, + "then": { + "type": "object", + "required": ["reason"] + }, + "else": { + "type": "object", + "not": { + "required": ["reason"] + } + } + } + ] + }, + "problem": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "https://mailschema.org/problems/refused", + "https://mailschema.org/problems/authentication-required", + "https://mailschema.org/problems/result-not-found", + "https://mailschema.org/problems/stale-target", + "https://mailschema.org/problems/expired-interaction", + "https://mailschema.org/problems/invalid-request", + "https://mailschema.org/problems/unsupported-type", + "https://mailschema.org/problems/unsupported-operation", + "https://mailschema.org/problems/idempotency-conflict", + "https://mailschema.org/problems/request-in-progress", + "https://mailschema.org/problems/already-decided" + ] + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "status": { + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "detail": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "instance": { + "$ref": "#/$defs/identifier" + }, + "profile": { + "$ref": "#/$defs/profile" + }, + "requestId": { + "$ref": "#/$defs/uuidUrn" + }, + "interactionId": { + "$ref": "#/$defs/uuidUrn" + }, + "code": { + "enum": [ + "refused", + "result-not-found", + "stale-target", + "expired-interaction", + "invalid-request", + "unsupported-type", + "unsupported-operation", + "idempotency-conflict", + "request-in-progress", + "already-decided" + ] + }, + "target": { + "$ref": "#/$defs/target" + }, + "errors": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "detail": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "pointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "maxLength": 1000 + } + }, + "required": ["detail", "pointer"] + } + } + }, + "required": ["type", "title", "status", "detail"], + "dependentRequired": { + "instance": ["profile", "requestId", "code"], + "profile": ["instance", "requestId", "code"], + "requestId": ["instance", "profile", "code"], + "interactionId": ["instance", "profile", "requestId", "code"], + "code": ["instance", "profile", "requestId"] + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "https://mailschema.org/problems/result-not-found", + "https://mailschema.org/problems/stale-target", + "https://mailschema.org/problems/expired-interaction", + "https://mailschema.org/problems/unsupported-type", + "https://mailschema.org/problems/unsupported-operation", + "https://mailschema.org/problems/idempotency-conflict", + "https://mailschema.org/problems/request-in-progress", + "https://mailschema.org/problems/already-decided" + ] + } + }, + "required": ["type"] + }, + "then": { + "required": ["code"] + } + }, + { + "if": { + "required": ["errors"] + }, + "then": { + "properties": { + "code": { + "const": "invalid-request" + } + }, + "required": ["code"] + } + }, + { + "if": { + "properties": { + "code": { + "not": { + "const": "result-not-found" + } + } + }, + "required": ["code"] + }, + "then": { + "required": ["interactionId"] + } + }, + { + "if": { + "properties": { + "code": { + "const": "invalid-request" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/invalid-request" + }, + "status": { + "const": 400 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "refused" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/refused" + }, + "status": { + "const": 403 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "result-not-found" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/result-not-found" + }, + "status": { + "const": 404 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "stale-target" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/stale-target" + }, + "status": { + "const": 409 + } + }, + "required": ["target"] + } + }, + { + "if": { + "properties": { + "code": { + "const": "idempotency-conflict" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/idempotency-conflict" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "request-in-progress" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/request-in-progress" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "already-decided" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/already-decided" + }, + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "expired-interaction" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/expired-interaction" + }, + "status": { + "const": 410 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "unsupported-type" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-type" + }, + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "code": { + "const": "unsupported-operation" + } + }, + "required": ["code"] + }, + "then": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-operation" + }, + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/invalid-request" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 400 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/authentication-required" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 401 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/refused" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 403 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/result-not-found" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 404 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/stale-target" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/idempotency-conflict" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/request-in-progress" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/already-decided" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 409 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/expired-interaction" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 410 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-type" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 422 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "https://mailschema.org/problems/unsupported-operation" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "status": { + "const": 422 + } + } + } + } + ] + }, + "detailsKey": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$" + }, + "detailsValue": { + "anyOf": [ + { + "type": "string", + "maxLength": 12000 + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/$defs/detailsValue" + } + }, + { + "type": "object", + "maxProperties": 100, + "propertyNames": { + "$ref": "#/$defs/detailsKey" + }, + "additionalProperties": { + "$ref": "#/$defs/detailsValue" + } + } + ] + } + } +} diff --git a/src/map.ts b/src/map.ts index 54708bb..013ba7c 100644 --- a/src/map.ts +++ b/src/map.ts @@ -1,69 +1,27 @@ -import Ajv2020 from 'ajv/dist/2020.js'; -import addFormats from 'ajv-formats'; -import mapSchema from './map-0.1.schema.json' with { type: 'json' }; -import contentReview01Schema from './content-review-0.1.schema.json' with { type: 'json' }; -import contentReview01Contract from './content-review-0.1.contract.json' with { type: 'json' }; -import contentReview02Schema from './content-review-0.2.schema.json' with { type: 'json' }; -import contentReview02Contract from './content-review-0.2.contract.json' with { type: 'json' }; +import { readFileSync } from 'node:fs'; -const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false }); -addFormats(ajv); -ajv.addSchema(mapSchema); -const mapDocument = ajv.getSchema(mapSchema.$id)!; -const contentReview01Request = ajv.compile(contentReview01Schema); -const contentReview02Request = ajv.compile(contentReview02Schema); +/** The MAP profile whose core artifacts this package carries. */ +export const MAP_PROFILE = 'https://mailschema.org/profiles/map/0.2'; -function errors(validate: typeof mapDocument, value: unknown): string[] { - if (validate(value)) return []; - return (validate.errors || []).map( - (error) => - `${error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`, - ); -} - -export function mapErrors(value: unknown): string[] { - return errors(mapDocument, value); -} - -export function assertMapDocument(value: unknown): void { - const found = mapErrors(value); - if (found.length) throw new Error(`Invalid MAP 0.1 document:\n${found.join('\n')}`); -} - -export function contentReviewRequestErrors(value: unknown): string[] { - return errors(contentReview02Request, value); -} - -export function assertContentReviewRequest(value: unknown): void { - const found = contentReviewRequestErrors(value); - if (found.length) throw new Error(`Invalid Content Review 0.2 request:\n${found.join('\n')}`); -} - -export function contentReview01RequestErrors(value: unknown): string[] { - return errors(contentReview01Request, value); -} - -export function assertContentReview01Request(value: unknown): void { - const found = contentReview01RequestErrors(value); - if (found.length) throw new Error(`Invalid Content Review 0.1 request:\n${found.join('\n')}`); -} +const artifact = (name: string): Record => + JSON.parse(readFileSync(new URL(`./${name}`, import.meta.url), 'utf8')); +/** A fresh copy of the MAP 0.2 core schema, as the profile record binds it by SHA-256. */ export function getMapSchema(): Record { - return structuredClone(mapSchema); -} - -export function getContentReviewSchema(): Record { - return structuredClone(contentReview02Schema); + return artifact('map-0.2.schema.json'); } -export function getContentReviewContract(): Record { - return structuredClone(contentReview02Contract); +/** A fresh copy of the MAP 0.2 JSON-LD context. */ +export function getMapContext(): Record { + return artifact('map-0.2.jsonld'); } -export function getContentReview01Schema(): Record { - return structuredClone(contentReview01Schema); +/** A fresh copy of the type contract format every MAP 0.2 contract follows. */ +export function getContractFormatSchema(): Record { + return artifact('type-contract-0.2.schema.json'); } -export function getContentReview01Contract(): Record { - return structuredClone(contentReview01Contract); +/** A fresh copy of the form fields block contracts pin. */ +export function getFormsSchema(): Record { + return artifact('forms-0.1.schema.json'); } diff --git a/src/type-contract-0.2.schema.json b/src/type-contract-0.2.schema.json new file mode 100644 index 0000000..1fe14b2 --- /dev/null +++ b/src/type-contract-0.2.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/schemas/type-contract-0.2.schema.json", + "title": "MailSchema type contract for MAP 0.2", + "description": "A versioned interaction contract: the details, inputs, results, authority, repeatability and consequences of each operation, with every schema it depends on pinned by digest.", + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "MapTypeContract" + }, + "id": { + "type": "string", + "pattern": "^https://mailschema\\.org/types/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 2048 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", + "maxLength": 80 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.2" + }, + "target": { + "$ref": "#/$defs/prose" + }, + "dependencies": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/$defs/schemaReference" + } + }, + "detailsSchema": { + "type": "object" + }, + "requestSchema": { + "$ref": "#/$defs/schemaReference" + }, + "operations": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/token" + }, + "effect": { + "$ref": "#/$defs/prose" + }, + "authority": { + "type": "array", + "items": { + "enum": ["credential", "possession"] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "consequences": { + "type": "array", + "items": { + "enum": [ + "refusal", + "protection", + "record", + "disclosure", + "commitment", + "authorization", + "assertion" + ] + }, + "minItems": 1, + "maxItems": 7, + "uniqueItems": true + }, + "repeatable": { + "type": "boolean" + }, + "fieldBindings": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "input": { + "$ref": "#/$defs/pointer" + }, + "fields": { + "$ref": "#/$defs/pointer" + } + }, + "required": ["input", "fields"] + } + }, + "results": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "enum": ["accepted", "completed", "failed", "pending", "approval-required"] + }, + "reasons": { + "type": "array", + "items": { + "$ref": "#/$defs/token" + }, + "minItems": 1, + "maxItems": 20, + "uniqueItems": true + }, + "outputSchema": { + "type": "object" + } + }, + "required": ["state", "outputSchema"] + } + } + }, + "required": ["id", "effect", "authority", "consequences", "results"] + } + } + }, + "required": [ + "kind", + "id", + "version", + "profile", + "target", + "dependencies", + "requestSchema", + "operations" + ], + "$defs": { + "prose": { + "type": "string", + "minLength": 1, + "maxLength": 12000, + "pattern": "[^\\t\\n\\u000b\\f\\r \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]" + }, + "token": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", + "maxLength": 100 + }, + "pointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)+$", + "maxLength": 1000 + }, + "schemaReference": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "pattern": "^https://mailschema\\.org/schemas/[a-z0-9.-]+\\.json$", + "maxLength": 2048 + }, + "canonicalDigest": { + "type": "string", + "pattern": "^sha-256:[a-f0-9]{64}$" + } + }, + "required": ["url", "canonicalDigest"] + } + } +} diff --git a/test/map-description.json b/test/map-description.json deleted file mode 100644 index 3bb8c3d..0000000 --- a/test/map-description.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.1.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10", - "profile": "https://mailschema.org/profiles/map/0.1", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.2", - "contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637" - }, - "describedAt": "2026-09-23T01:00:00Z", - "expiresAt": "2026-09-30T01:00:00Z", - "service": { - "id": "https://reviews.example/service", - "name": "Example Review Service", - "execution": { - "url": "https://reviews.example/map/actions", - "method": "POST", - "requestMediaType": "application/json", - "resultMediaType": "application/json", - "resultUrlTemplate": "https://reviews.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://reviews.example/reviews/campaign-42/revision/4", - "authorization": { - "kind": "service-configured", - "schemes": [ - "oauth2" - ], - "audience": "https://reviews.example/" - } - }, - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:9096ce5e9226696d3bdc80f7d62a8e1ca4995c4fd8c4b9f0155287529aefe8fd" - }, - "operations": [ - { - "id": "request-changes", - "name": "Request changes", - "description": "Record feedback on revision 4.", - "inputSchema": "https://mailschema.org/schemas/content-review-0.2.schema.json" - }, - { - "id": "approve", - "name": "Approve", - "description": "Record approval of revision 4.", - "inputSchema": "https://mailschema.org/schemas/content-review-0.2.schema.json" - } - ] -} diff --git a/test/map-request.json b/test/map-request.json deleted file mode 100644 index 47e5dfe..0000000 --- a/test/map-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.1", - "requestId": "urn:uuid:018f47a2-b4d3-7c02-b491-7bdf2eaac67c", - "interactionId": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.2", - "contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637" - }, - "operation": "approve", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "digest": "sha-256:9096ce5e9226696d3bdc80f7d62a8e1ca4995c4fd8c4b9f0155287529aefe8fd" - }, - "input": {} -} diff --git a/test/map-result.json b/test/map-result.json deleted file mode 100644 index abd730a..0000000 --- a/test/map-result.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.1", - "requestId": "urn:uuid:018f47a2-b4d3-7c02-b491-7bdf2eaac67c", - "interactionId": "urn:uuid:018f47a2-5d7c-7b11-9a3d-4d2160b85b10", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.2", - "contractDigest": "sha-256:6ee5b086138799f70f2489e851e1db6e11ff91cd1141bd36fe202a47d8c1e637" - }, - "operation": "approve", - "state": "completed", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "digest": "sha-256:9096ce5e9226696d3bdc80f7d62a8e1ca4995c4fd8c4b9f0155287529aefe8fd" - }, - "recordedAt": "2026-09-23T01:06:01Z", - "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A018f47a2-b4d3-7c02-b491-7bdf2eaac67c", - "output": { - "decision": "approved" - } -} diff --git a/test/new-type.json b/test/new-type.json new file mode 100644 index 0000000..5121822 --- /dev/null +++ b/test/new-type.json @@ -0,0 +1,55 @@ +{ + "format": "mailschema-contribution/1", + "contributor": { + "name": "Example Document Service", + "url": "https://example.com" + }, + "id": "example-document-receipt", + "kind": "new-type", + "summary": "Illustrative new-type contribution. Replace this example with your own workflow and attribution.", + "record": { + "slug": "document-receipt", + "name": "Document Receipt", + "summary": "Acknowledge receipt of a specified document.", + "category": "Documents", + "status": "Proposal", + "version": null, + "profile": "Under consideration", + "overview": "A service delivers a document and asks the recipient to acknowledge that they received it.", + "target": "The delivered document and its revision.", + "operations": [ + { + "id": "acknowledge-receipt", + "name": "Acknowledge receipt", + "description": "Record receipt of the identified document." + } + ], + "inputs": "The delivery reference and document revision.", + "results": "Receipt recorded, or an explanation of why the acknowledgement was not recorded.", + "permissions": "The service checks that the caller may acknowledge receipt for the intended recipient.", + "humanRoute": "A document page with the same acknowledgement operation.", + "example": { + "title": "Acknowledge a revised delivery note", + "steps": [ + "The service delivers revision 2 of a delivery note.", + "The recipient acknowledges receipt.", + "The service records receipt of that revision." + ], + "exception": "Receipt does not indicate agreement with the document or approval of a separate operation." + }, + "references": [ + { + "title": "Schema.org Actions", + "href": "https://schema.org/docs/actions.html", + "description": "Candidate vocabulary to assess when describing the acknowledgement operation." + } + ], + "openQuestions": ["Document revision references and retry behaviour."], + "maintainers": [ + { + "name": "Example Document Service", + "url": "https://example.com" + } + ] + } +} diff --git a/test/package.test.mjs b/test/package.test.mjs index 6f33e5f..e5032cb 100644 --- a/test/package.test.mjs +++ b/test/package.test.mjs @@ -3,41 +3,38 @@ import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { - assertContentReviewRequest, - assertMapDocument, + MAP_PROFILE, + assertContribution, + getContractFormatSchema, getContributionSchema, - getContentReviewContract, - getContentReview01Contract, + getFormsSchema, + getMapContext, getMapSchema, } from '../dist/index.js'; const load = async (name) => JSON.parse(await readFile(new URL(name, import.meta.url), 'utf8')); -test('exposes the canonical schema identifiers', () => { - assert.equal(getMapSchema().$id, 'https://mailschema.org/schemas/map-0.1.schema.json'); - assert.equal(getContentReviewContract().id, 'https://mailschema.org/types/content-review'); - assert.equal(getContentReviewContract().version, '0.2'); - assert.equal(getContentReview01Contract().version, '0.1'); +test('exposes the MAP 0.2 core artifacts', () => { + assert.equal(MAP_PROFILE, 'https://mailschema.org/profiles/map/0.2'); + assert.equal(getMapSchema().$id, 'https://mailschema.org/schemas/map-0.2.schema.json'); + assert.equal(getMapContext()['@context'].MailAction, 'map:MailAction'); + assert.equal( + getContractFormatSchema().$id, + 'https://mailschema.org/schemas/type-contract-0.2.schema.json', + ); + assert.equal(getFormsSchema().$id, 'https://mailschema.org/schemas/forms-0.1.schema.json'); assert.equal(getContributionSchema().$schema, 'https://json-schema.org/draft/2020-12/schema'); }); -test('validates MAP and Content Review fixtures', async () => { - assertMapDocument(await load('map-description.json')); - assertContentReviewRequest(await load('map-request.json')); - const result = await load('map-result.json'); - assertMapDocument(result); - assert.equal(result.type.id, 'https://mailschema.org/types/content-review'); +test('validates a Registry contribution', async () => { + assertContribution(await load('new-type.json')); }); -test('preserves canonical schema bytes during the build', async () => { - for (const name of [ - 'contribution.schema.json', - 'map-0.1.schema.json', - 'content-review-0.1.schema.json', - 'content-review-0.1.contract.json', - 'content-review-0.2.schema.json', - 'content-review-0.2.contract.json', - ]) { +test('preserves canonical artifact bytes during the build', async () => { + const artifacts = JSON.parse( + await readFile(new URL('../artifacts.json', import.meta.url), 'utf8'), + ); + for (const name of artifacts) { const source = await readFile(new URL(`../src/${name}`, import.meta.url)); const built = await readFile(new URL(`../dist/${name}`, import.meta.url)); assert.deepEqual(built, source);