From 8e02f51284be36e165f4f07b4d15dadc492f7230 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 7 Oct 2026 14:59:10 +1100 Subject: [PATCH] feat: process MAP 0.3 and release 0.3.0 --- README.md | 144 +- UPSTREAM.md | 16 +- artifacts.json | 10 +- bin/mailschema.js | 93 +- dist/context.jsonld | 50 + dist/contract.schema.json | 206 ++ dist/contribution.schema.json | 874 ------ dist/core.schema.json | 210 ++ dist/core/artifacts.d.ts | 47 +- dist/core/artifacts.js | 90 +- dist/core/binding.d.ts | 24 - dist/core/binding.js | 50 - dist/core/bundled.d.ts | 8 +- dist/core/bundled.js | 14 +- dist/core/contract.d.ts | 115 +- dist/core/contract.js | 522 ++-- dist/core/description.d.ts | 45 + dist/core/description.js | 47 + dist/core/document.d.ts | 24 - dist/core/documents.d.ts | 68 - dist/core/documents.js | 145 - dist/core/implementation.d.ts | 32 + dist/core/implementation.js | 25 + dist/core/index.d.ts | 13 +- dist/core/index.js | 18 +- dist/core/json.d.ts | 23 + dist/core/{document.js => json.js} | 112 +- dist/core/limits.d.ts | 19 - dist/core/limits.js | 55 - dist/core/patterns.d.ts | 12 + dist/core/patterns.js | 165 ++ dist/core/pointer.d.ts | 12 - dist/core/pointer.js | 48 - dist/core/schemas.d.ts | 21 - dist/core/schemas.js | 125 - dist/core/sha256.d.ts | 2 + dist/core/sha256.js | 59 + dist/core/types.d.ts | 127 - dist/core/types.js | 2 - dist/forms-0.1.schema.json | 329 --- dist/implementation.schema.json | 173 ++ dist/index.d.ts | 13 +- dist/index.js | 16 +- dist/map-0.2.jsonld | 91 - dist/map-0.2.schema.json | 1033 ------- dist/map.d.ts | 8 - dist/map.js | 18 - dist/model.d.ts | 95 - dist/model.js | 5 - dist/profile.json | 27 + dist/type-contract-0.2.schema.json | 179 -- dist/validation.d.ts | 15 - dist/validation.js | 64 - package-lock.json | 4 +- package.json | 19 +- src/context.jsonld | 50 + src/contract.schema.json | 206 ++ src/contribution.schema.json | 874 ------ src/core.schema.json | 210 ++ src/core/artifacts.ts | 118 +- src/core/binding.ts | 69 - src/core/bundled.ts | 14 +- src/core/contract.ts | 585 ++-- src/core/description.ts | 68 + src/core/documents.ts | 229 -- src/core/implementation.ts | 45 + src/core/index.ts | 75 +- src/core/{document.ts => json.ts} | 121 +- src/core/limits.ts | 57 - src/core/patterns.ts | 135 + src/core/pointer.ts | 54 - src/core/schemas.ts | 133 - src/core/sha256.ts | 63 + src/core/types.ts | 108 - src/forms-0.1.schema.json | 329 --- src/implementation.schema.json | 173 ++ src/index.ts | 27 +- src/map-0.2.jsonld | 91 - src/map-0.2.schema.json | 1033 ------- src/map.ts | 24 - src/model.ts | 64 - src/profile.json | 27 + src/type-contract-0.2.schema.json | 179 -- src/validation.ts | 87 - .../contracts/account-activity-0.1.json | 109 - .../account-security-response-0.1.json | 113 + .../contracts/action-approval-0.1.json | 170 -- .../contracts/campaign-send-approval-0.1.json | 262 ++ .../contracts/content-review-0.3.json | 113 - .../email-address-confirmation-0.1.json | 85 + .../contracts/email-confirmation-0.1.json | 80 - .../contracts/event-response-0.1.json | 114 - .../contracts/information-request-0.1.json | 87 - .../contracts/meeting-scheduling-0.1.json | 126 - .../contracts/payment-request-0.1.json | 151 - .../contracts/publication-approval-0.1.json | 284 ++ .../subscription-preferences-0.1.json | 66 - .../contracts/task-assignment-0.1.json | 140 - .../examples/account-security-response.json | 50 + .../examples/campaign-send-approval.json | 64 + .../examples/email-address-confirmation.json | 45 + .../examples/publication-approval.json | 61 + test/fixtures/jcs-vectors.json | 56 + .../ijson-vectors.json => json-vectors.json} | 23 +- .../account-activity/confirm.request.json | 14 - .../account-activity/confirm.result.json | 26 - .../map-0.2/account-activity/description.json | 54 - .../report-unrecognized.request.json | 16 - .../report-unrecognized.result.json | 26 - .../approve.approval-required.json | 25 - .../action-approval/approve.declined.json | 25 - .../action-approval/approve.request.json | 14 - .../action-approval/approve.result.json | 26 - .../action-approval/decline.request.json | 16 - .../action-approval/decline.result.json | 26 - .../map-0.2/action-approval/description.json | 65 - .../approve.approval-required.json | 25 - .../content-review/approve.declined.json | 25 - .../content-review/approve.request.json | 14 - .../content-review/approve.result.json | 26 - .../map-0.2/content-review/description.json | 52 - .../request-changes.request.json | 16 - .../request-changes.result.json | 27 - .../content-review/stale-target.problem.json | 17 - .../email-confirmation/confirm.request.json | 14 - .../email-confirmation/confirm.result.json | 25 - .../email-confirmation/description.json | 49 - .../report-unrecognized.request.json | 14 - .../report-unrecognized.result.json | 25 - .../event-response/accept.request.json | 14 - .../map-0.2/event-response/accept.result.json | 25 - .../event-response/decline.request.json | 16 - .../event-response/decline.result.json | 25 - .../map-0.2/event-response/description.json | 54 - .../map-0.2/event-response/invitation.ics | 15 - .../respond-tentatively.request.json | 16 - .../respond-tentatively.result.json | 25 - .../information-request/decline.request.json | 14 - .../information-request/decline.result.json | 25 - .../information-request/description.json | 73 - .../invalid-request.problem.json | 17 - .../submit-response.request.json | 20 - .../submit-response.result.json | 25 - test/fixtures/map-0.2/jcs-vectors.json | 56 - test/fixtures/map-0.2/lexical-vectors.json | 2618 ----------------- test/fixtures/map-0.2/media-type-vectors.json | 86 - .../already-decided.problem.json | 11 - .../meeting-scheduling/book.request.json | 19 - .../meeting-scheduling/book.result.json | 27 - .../meeting-scheduling/book.unavailable.json | 24 - .../meeting-scheduling/decline.request.json | 14 - .../meeting-scheduling/decline.result.json | 25 - .../meeting-scheduling/description.json | 82 - .../payment-request/accept.request.json | 16 - .../payment-request/accept.result.json | 27 - .../payment-request/decline.request.json | 16 - .../payment-request/decline.result.json | 26 - .../map-0.2/payment-request/description.json | 64 - .../subscription-preferences/description.json | 87 - .../update-preferences.request.json | 18 - .../update-preferences.result.json | 25 - .../task-assignment/accept.request.json | 14 - .../task-assignment/accept.result.json | 26 - .../map-0.2/task-assignment/assignment.ics | 14 - .../task-assignment/decline.request.json | 16 - .../task-assignment/decline.result.json | 26 - .../map-0.2/task-assignment/description.json | 61 - .../report-completion.request.json | 19 - .../report-completion.result.json | 26 - .../report-progress.request.json | 16 - .../report-progress.result.json | 26 - test/fixtures/profile.json | 30 - .../schemas/account-activity-0.1.schema.json | 62 - .../schemas/action-approval-0.1.schema.json | 62 - .../schemas/content-review-0.3.schema.json | 63 - .../email-confirmation-0.1.schema.json | 62 - .../schemas/event-response-0.1.schema.json | 90 - .../information-request-0.1.schema.json | 68 - .../meeting-scheduling-0.1.schema.json | 72 - .../schemas/payment-request-0.1.schema.json | 68 - .../subscription-preferences-0.1.schema.json | 44 - .../schemas/task-assignment-0.1.schema.json | 125 - test/fixtures/shape-vectors.json | 311 ++ test/new-type.json | 55 - test/package.test.mjs | 198 +- tsconfig.json | 2 +- 186 files changed, 4335 insertions(+), 14696 deletions(-) create mode 100644 dist/context.jsonld create mode 100644 dist/contract.schema.json delete mode 100644 dist/contribution.schema.json create mode 100644 dist/core.schema.json delete mode 100644 dist/core/binding.d.ts delete mode 100644 dist/core/binding.js create mode 100644 dist/core/description.d.ts create mode 100644 dist/core/description.js delete mode 100644 dist/core/document.d.ts delete mode 100644 dist/core/documents.d.ts delete mode 100644 dist/core/documents.js create mode 100644 dist/core/implementation.d.ts create mode 100644 dist/core/implementation.js create mode 100644 dist/core/json.d.ts rename dist/core/{document.js => json.js} (58%) delete mode 100644 dist/core/limits.d.ts delete mode 100644 dist/core/limits.js create mode 100644 dist/core/patterns.d.ts create mode 100644 dist/core/patterns.js delete mode 100644 dist/core/pointer.d.ts delete mode 100644 dist/core/pointer.js delete mode 100644 dist/core/schemas.d.ts delete mode 100644 dist/core/schemas.js create mode 100644 dist/core/sha256.d.ts create mode 100644 dist/core/sha256.js delete mode 100644 dist/core/types.d.ts delete mode 100644 dist/core/types.js delete mode 100644 dist/forms-0.1.schema.json create mode 100644 dist/implementation.schema.json delete mode 100644 dist/map-0.2.jsonld delete mode 100644 dist/map-0.2.schema.json delete mode 100644 dist/map.d.ts delete mode 100644 dist/map.js delete mode 100644 dist/model.d.ts delete mode 100644 dist/model.js create mode 100644 dist/profile.json delete mode 100644 dist/type-contract-0.2.schema.json delete mode 100644 dist/validation.d.ts delete mode 100644 dist/validation.js create mode 100644 src/context.jsonld create mode 100644 src/contract.schema.json delete mode 100644 src/contribution.schema.json create mode 100644 src/core.schema.json delete mode 100644 src/core/binding.ts create mode 100644 src/core/description.ts delete mode 100644 src/core/documents.ts create mode 100644 src/core/implementation.ts rename src/core/{document.ts => json.ts} (53%) delete mode 100644 src/core/limits.ts create mode 100644 src/core/patterns.ts delete mode 100644 src/core/pointer.ts delete mode 100644 src/core/schemas.ts create mode 100644 src/core/sha256.ts delete mode 100644 src/core/types.ts delete mode 100644 src/forms-0.1.schema.json create mode 100644 src/implementation.schema.json delete mode 100644 src/map-0.2.jsonld delete mode 100644 src/map-0.2.schema.json delete mode 100644 src/map.ts delete mode 100644 src/model.ts create mode 100644 src/profile.json delete mode 100644 src/type-contract-0.2.schema.json delete mode 100644 src/validation.ts delete mode 100644 test/fixtures/contracts/account-activity-0.1.json create mode 100644 test/fixtures/contracts/account-security-response-0.1.json delete mode 100644 test/fixtures/contracts/action-approval-0.1.json create mode 100644 test/fixtures/contracts/campaign-send-approval-0.1.json delete mode 100644 test/fixtures/contracts/content-review-0.3.json create mode 100644 test/fixtures/contracts/email-address-confirmation-0.1.json delete mode 100644 test/fixtures/contracts/email-confirmation-0.1.json delete mode 100644 test/fixtures/contracts/event-response-0.1.json delete mode 100644 test/fixtures/contracts/information-request-0.1.json delete mode 100644 test/fixtures/contracts/meeting-scheduling-0.1.json delete mode 100644 test/fixtures/contracts/payment-request-0.1.json create mode 100644 test/fixtures/contracts/publication-approval-0.1.json delete mode 100644 test/fixtures/contracts/subscription-preferences-0.1.json delete mode 100644 test/fixtures/contracts/task-assignment-0.1.json create mode 100644 test/fixtures/examples/account-security-response.json create mode 100644 test/fixtures/examples/campaign-send-approval.json create mode 100644 test/fixtures/examples/email-address-confirmation.json create mode 100644 test/fixtures/examples/publication-approval.json create mode 100644 test/fixtures/jcs-vectors.json rename test/fixtures/{map-0.2/ijson-vectors.json => json-vectors.json} (99%) delete mode 100644 test/fixtures/map-0.2/account-activity/confirm.request.json delete mode 100644 test/fixtures/map-0.2/account-activity/confirm.result.json delete mode 100644 test/fixtures/map-0.2/account-activity/description.json delete mode 100644 test/fixtures/map-0.2/account-activity/report-unrecognized.request.json delete mode 100644 test/fixtures/map-0.2/account-activity/report-unrecognized.result.json delete mode 100644 test/fixtures/map-0.2/action-approval/approve.approval-required.json delete mode 100644 test/fixtures/map-0.2/action-approval/approve.declined.json delete mode 100644 test/fixtures/map-0.2/action-approval/approve.request.json delete mode 100644 test/fixtures/map-0.2/action-approval/approve.result.json delete mode 100644 test/fixtures/map-0.2/action-approval/decline.request.json delete mode 100644 test/fixtures/map-0.2/action-approval/decline.result.json delete mode 100644 test/fixtures/map-0.2/action-approval/description.json delete mode 100644 test/fixtures/map-0.2/content-review/approve.approval-required.json delete mode 100644 test/fixtures/map-0.2/content-review/approve.declined.json delete mode 100644 test/fixtures/map-0.2/content-review/approve.request.json delete mode 100644 test/fixtures/map-0.2/content-review/approve.result.json delete mode 100644 test/fixtures/map-0.2/content-review/description.json delete mode 100644 test/fixtures/map-0.2/content-review/request-changes.request.json delete mode 100644 test/fixtures/map-0.2/content-review/request-changes.result.json delete mode 100644 test/fixtures/map-0.2/content-review/stale-target.problem.json delete mode 100644 test/fixtures/map-0.2/email-confirmation/confirm.request.json delete mode 100644 test/fixtures/map-0.2/email-confirmation/confirm.result.json delete mode 100644 test/fixtures/map-0.2/email-confirmation/description.json delete mode 100644 test/fixtures/map-0.2/email-confirmation/report-unrecognized.request.json delete mode 100644 test/fixtures/map-0.2/email-confirmation/report-unrecognized.result.json delete mode 100644 test/fixtures/map-0.2/event-response/accept.request.json delete mode 100644 test/fixtures/map-0.2/event-response/accept.result.json delete mode 100644 test/fixtures/map-0.2/event-response/decline.request.json delete mode 100644 test/fixtures/map-0.2/event-response/decline.result.json delete mode 100644 test/fixtures/map-0.2/event-response/description.json delete mode 100644 test/fixtures/map-0.2/event-response/invitation.ics delete mode 100644 test/fixtures/map-0.2/event-response/respond-tentatively.request.json delete mode 100644 test/fixtures/map-0.2/event-response/respond-tentatively.result.json delete mode 100644 test/fixtures/map-0.2/information-request/decline.request.json delete mode 100644 test/fixtures/map-0.2/information-request/decline.result.json delete mode 100644 test/fixtures/map-0.2/information-request/description.json delete mode 100644 test/fixtures/map-0.2/information-request/invalid-request.problem.json delete mode 100644 test/fixtures/map-0.2/information-request/submit-response.request.json delete mode 100644 test/fixtures/map-0.2/information-request/submit-response.result.json delete mode 100644 test/fixtures/map-0.2/jcs-vectors.json delete mode 100644 test/fixtures/map-0.2/lexical-vectors.json delete mode 100644 test/fixtures/map-0.2/media-type-vectors.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/already-decided.problem.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/book.request.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/book.result.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/book.unavailable.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/decline.request.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/decline.result.json delete mode 100644 test/fixtures/map-0.2/meeting-scheduling/description.json delete mode 100644 test/fixtures/map-0.2/payment-request/accept.request.json delete mode 100644 test/fixtures/map-0.2/payment-request/accept.result.json delete mode 100644 test/fixtures/map-0.2/payment-request/decline.request.json delete mode 100644 test/fixtures/map-0.2/payment-request/decline.result.json delete mode 100644 test/fixtures/map-0.2/payment-request/description.json delete mode 100644 test/fixtures/map-0.2/subscription-preferences/description.json delete mode 100644 test/fixtures/map-0.2/subscription-preferences/update-preferences.request.json delete mode 100644 test/fixtures/map-0.2/subscription-preferences/update-preferences.result.json delete mode 100644 test/fixtures/map-0.2/task-assignment/accept.request.json delete mode 100644 test/fixtures/map-0.2/task-assignment/accept.result.json delete mode 100644 test/fixtures/map-0.2/task-assignment/assignment.ics delete mode 100644 test/fixtures/map-0.2/task-assignment/decline.request.json delete mode 100644 test/fixtures/map-0.2/task-assignment/decline.result.json delete mode 100644 test/fixtures/map-0.2/task-assignment/description.json delete mode 100644 test/fixtures/map-0.2/task-assignment/report-completion.request.json delete mode 100644 test/fixtures/map-0.2/task-assignment/report-completion.result.json delete mode 100644 test/fixtures/map-0.2/task-assignment/report-progress.request.json delete mode 100644 test/fixtures/map-0.2/task-assignment/report-progress.result.json delete mode 100644 test/fixtures/profile.json delete mode 100644 test/fixtures/schemas/account-activity-0.1.schema.json delete mode 100644 test/fixtures/schemas/action-approval-0.1.schema.json delete mode 100644 test/fixtures/schemas/content-review-0.3.schema.json delete mode 100644 test/fixtures/schemas/email-confirmation-0.1.schema.json delete mode 100644 test/fixtures/schemas/event-response-0.1.schema.json delete mode 100644 test/fixtures/schemas/information-request-0.1.schema.json delete mode 100644 test/fixtures/schemas/meeting-scheduling-0.1.schema.json delete mode 100644 test/fixtures/schemas/payment-request-0.1.schema.json delete mode 100644 test/fixtures/schemas/subscription-preferences-0.1.schema.json delete mode 100644 test/fixtures/schemas/task-assignment-0.1.schema.json create mode 100644 test/fixtures/shape-vectors.json delete mode 100644 test/new-type.json diff --git a/README.md b/README.md index 5c1eb2b..7102bd5 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,44 @@ -# MailSchema for JavaScript +# mailschema -[![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) - -Mail Action Protocol 0.2 for JavaScript and TypeScript. The package parses, canonicalizes, digests and validates MAP documents, verifies the type contracts an implementation vendors, and builds results and problems. It also carries the MAP 0.2 core artifacts and validates MailSchema Registry contributions locally. - -It does not establish endpoint trust, verify email authentication, grant authority or send email. Possession mode needs DKIM and DMARC checks on the raw message, which this package leaves to the client. - -[Specification](https://mailschema.org/specification) · [Registry](https://mailschema.org/registry) · [Tools](https://mailschema.org/tools) · [Source](https://github.com/mailschema/javascript) - -## Install +[Mail Action Protocol](https://mailschema.org) 0.3 for JavaScript. It parses and checks MAP descriptions, type contracts and Registry implementation records, and computes the RFC 8785 digests that identify contracts. The same code runs the MailSchema specification's own checks. It has no Node.js-only dependencies, so it also runs in browsers and workers. ```sh -npm install mailschema -``` - -Node.js 22 or newer is required. The package is ESM and includes TypeScript declarations. - -## A service - -Vendor the exact contract and request schema your service implements, and pin the contract by the digest you reviewed. The package refuses any other contract. - -```ts -import { readFileSync } from 'node:fs'; -import { Contract, digest } from 'mailschema'; - -const read = (path: string) => JSON.parse(readFileSync(path, 'utf8')); -const contract = new Contract( - read('mailschema/content-review-0.3.json'), - read('mailschema/content-review-0.3.schema.json'), - { digest: 'sha-256:…' }, -); -``` - -Before sending a description, check it against every rule of the core and the contract, and store its digest or keep it rebuildable from the interaction: - -```ts -contract.descriptionErrors(description); // [] -digest(description); // 'sha-256:…' +npm install mailschema@0.3.0 ``` -When a request arrives, check it in this order. No step applies an effect for a problem, and a refusal before the description is resolved and its digest matched leaves the request identifier unclaimed: - -```ts -import { isJsonRequest, parse, problem, requestErrors, result, resultUrl } from 'mailschema'; -import type { MapRequest } from 'mailschema'; - -// Answer 415 unless isJsonRequest(contentType). Under possession authority, answer an -// unknown capability with a plain 404 and a lapsed one with a plain 410 before reading -// the body; under credential authority, authenticate the principal. -const request = parse(body) as MapRequest; // throws InvalidDocument -requestErrors(request); // any errors: invalid-request - -// Resolve the description you issued for request.interactionId. Refuse an unknown -// interaction, or a request whose descriptionDigest is not digest(description). +## Check a description against its contract -// Request identifiers are claimed within your tenant: the account, or the capability. -// If request.requestId is already claimed there, refuse another principal, and a principal -// whose permission has been revoked, with an uncorrelated refused problem (problem() -// without requestId). Answer any change to the request, compared by digest, with -// idempotency-conflict. Otherwise settle the recorded result with settle() and return it, -// or expired-interaction once retainUntil() has passed. Nothing is applied again. - -const refused = contract.requestProblem(description, request, { now: new Date() }); -// refused.code is unsupported-type, unsupported-operation or expired-interaction, with the -// title and detail problem() takes. Then your own state: already-decided, or stale-target. +```js +import { readFile } from 'node:fs/promises'; +import { Contract, parseDescription } from 'mailschema'; -contract.inputErrors(description, request); -// [{ detail: '…', pointer: '/feedback' }], a claimed invalid-request +const contract = Contract.parse(await readFile('campaign-send-approval-0.1.json')); +const description = parseDescription(await readFile('description.json')); +const errors = contract.descriptionErrors(description); +if (errors.length) throw new Error(errors.join('\n')); ``` -Then apply the approval lifecycle or the effect, and record the result: - -```ts -result(request, { - state: 'accepted', - target: description.target, - resultUrl: resultUrl(description, request.requestId), - recordedAt: new Date(), - output: { feedbackRecorded: true, feedbackId: 'fb-1' }, -}); -``` +`parseDescription` reads the bytes as MAP JSON (strict I-JSON within 64 KiB) and checks the core rules, or throws `InvalidDocument` with every reason. Obtain contracts from a catalogue you configure and pin them by digest; a message never chooses one. ## API -| Call | Does | -| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `parse(text \| bytes)` | Parses a MAP document as I-JSON within the core limits, or throws `InvalidDocument` | -| `canonicalize(value)`, `digest(value)` | RFC 8785 canonical JSON, and `sha-256:` over it | -| `mapErrors(document)` | Checks a description, request, result or problem against the core schema | -| `descriptionErrors`, `requestErrors`, `resultErrors`, `problemErrors` | Check one document against its core definition only | -| `new Contract(contract, requestSchema, { digest, dependencies })` | Checks the contract's format and profile, refuses any contract but the one pinned by `digest`, checks its pinned dependencies and request schema, and compiles every validator once, or throws `InvalidContract`; `dependencies` supplies, by URL, any pinned schema the package does not bundle | -| `contract.descriptionErrors(description)` | Checks a description against every rule of the core and the contract | -| `contract.requestProblem(description, request, { now })` | The exact type, an offered operation the authority permits, and expiry, as a problem code, title and detail, or undefined | -| `contract.inputErrors(description, request)` | Checks an operation's input and field bindings, with JSON Pointers into `input` | -| `contract.requestErrors(request)`, `contract.resultErrors(result)` | Check a request against the core and the request schema, and a result against the core, the declared output schema and the declared reason | -| `contract.typeReference`, `contract.operation(id)`, `contract.isDecision(id)` | The type reference descriptions and requests name, one of its operations, and whether completing it decides the interaction | -| `result`, `transition`, `problem` | Build results, state transitions and problems with their correlation members, or throw rather than return a document the core refuses. A problem stays within the core limits | -| `resultUrl`, `resultStatus`, `isRequestId` | The result resource of a request, a result's HTTP status, and whether a value is a request identifier, the only thing a result URL names | -| `isJsonRequest(contentType)` | Whether a `Content-Type` admits a request rather than a 415 | -| `DESCRIPTION_MEDIA_TYPE`, `isDescriptionPart(mediaType, profileParameter)` | The `Content-Type` of the part that carries a description, and whether a designated part is labelled with this profile | -| `retainUntil`, `settle`, `reached` | Result retention, the expiry of an undecided approval, and whether a deadline has passed | -| `capability(description)`, `writtenPath(url)` | The possession capability: the last segment of the execution URL's path as written | +- `parse(input, maxBytes)`, `canonicalize(value)`, `digest(value)`: MAP JSON and RFC 8785 digests. +- `descriptionErrors(value)`, `parseDescription(input)`: the core description rules. +- `new Contract(value)`, `Contract.parse(input)`: a checked contract with `digest`, `operation(id)`, `detailsErrors(details)`, `inputErrors(operationId, input)` and `descriptionErrors(description)`. +- `contractErrors(value)`: why a value is not a contract, including schema keywords, formats and patterns every implementation can enforce alike. +- `implementationErrors(value)`, `parseImplementation(input)`: Registry implementation records. +- `PROFILE`, `CONTEXT`, `DESCRIPTION_MEDIA_TYPE`, `EFFECTS`, `CAPABILITY_KINDS` and the size limits. -Type rules a contract cannot express, such as a meeting slot being one of the slots offered, belong to each type's implementation. Validation uses Ajv with JSON Schema 2020-12. Lexical forms are core schema patterns, so `format` is never asserted, and references resolve only against pinned schemas, never over the network. - -## MAP 0.2 core artifacts - -```js -import { getContractFormatSchema, getFormsSchema, getMapContext, getMapSchema } from 'mailschema'; -``` - -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: an implementation vendors the contracts it supports, and `Contract` verifies them by digest. - -## Registry data - -```js -import { assertContribution, contributionErrors, referenceErrors } from 'mailschema'; - -const errors = contributionErrors(candidate); -if (errors.length) console.error(errors); -else assertContribution(candidate); - -const referenceProblems = referenceErrors(candidate, catalog); -``` +The profile record, context and schemas are exported byte for byte: `mailschema/profile.json`, `mailschema/context.jsonld`, `mailschema/core.schema.json`, `mailschema/contract.schema.json` and `mailschema/implementation.schema.json`. -`assertTypeRecord` and `getRecordSchema` handle expanded Registry records. Reference checks use a catalogue supplied by the caller; the package never fetches one. +## Command line ```sh -npx mailschema check contribution.json -npx mailschema check record.json --record -npx mailschema schema --map +npx mailschema@0.3.0 contract campaign-send-approval-0.1.json +npx mailschema@0.3.0 description description.json --contract campaign-send-approval-0.1.json +npx mailschema@0.3.0 implementation record.json ``` -## Trust boundary +Checking does not authenticate a message, establish a service's authority or perform an action. Those belong to the implementation, under the [specification](https://mailschema.org/specification/core). -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. +Source: [mailschema/javascript](https://github.com/mailschema/javascript). License: MIT. diff --git a/UPSTREAM.md b/UPSTREAM.md index 6997d85..f7225ec 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -1,13 +1,13 @@ # Release provenance -Version `0.2.2` derives from [`mailschema/mailschema@48c5fd6`](https://github.com/mailschema/mailschema/commit/48c5fd6f189940beb0675f6c1340d5c4c8b3c592). Its `src/core` is that commit's `src/map/core`, the MAP core the reference implementation and conformance suite run on, with only the bundled artifact imports pointing at this package's copies. +Version `0.3.0` derives from [`mailschema/mailschema@2c26469`](https://github.com/mailschema/mailschema/commit/2c26469209070f04ff9f8ec29e1387b8699a46a0). Its sources, tests and bundled files are prepared from that commit by `npm run packages:prepare`. -The bundled files are exact projections of that source commit and do not independently define MAP: +The bundled MAP 0.3 artifacts are exact copies of that commit's files and do not independently define MAP: -- MAP 0.2 core schema: `732d60fcb758d945dc15c57d885c9d1c0c76423a7439096d9755e8ec9830836a` -- MAP 0.2 JSON-LD context: `dfed0189524d7d16d31ed3d8af97e9a0c458200c4c034b413f9d14fa35b60257` -- Type contract format: `e030007cc034662c9d9d448a8a035aff3106cfb96fb60e6aa9f92b99b25df0b9` -- Form fields block: `3a976b93a057380e3630ca05e77e221a43a98559fb7fda8066299ec4bf2a8f9e` -- Contribution schema: `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564` +- profile (`dist/profile.json`): `d9481627f855c127a4f5d6ddb7f3b699f66f6475bc8672d609f5b499f83d1a0f` +- context (`dist/context.jsonld`): `a51490d1fe8af695c14ea508aad88fd0429d18564791b6b52f3ab07d5c51aab3` +- core-schema (`dist/core.schema.json`): `0ad09a6955a47e7a2c1c1eec0747a3e0b15c08a79860e90ce4e65da11983aeb3` +- contract-schema (`dist/contract.schema.json`): `86bf057529c4fd2c7f0a880825774afeed31e9f7ba1d99760858a585fe28d18d` +- implementation-schema (`dist/implementation.schema.json`): `c62acc3ebb9f384c8fba7be353ab1905b65883ac2b16b147fd98485eec643396` -The canonical artifacts 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 package and its release history. diff --git a/artifacts.json b/artifacts.json index cb8b4ff..684b7b7 100644 --- a/artifacts.json +++ b/artifacts.json @@ -1,7 +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" + "profile.json", + "context.jsonld", + "core.schema.json", + "contract.schema.json", + "implementation.schema.json" ] diff --git a/bin/mailschema.js b/bin/mailschema.js index d8c83e4..ad5cb71 100755 --- a/bin/mailschema.js +++ b/bin/mailschema.js @@ -1,72 +1,59 @@ #!/usr/bin/env node -import { open } from 'node:fs/promises'; +import { readFile, stat } from 'node:fs/promises'; import { parseArgs } from 'node:util'; import { - assertContribution, - assertTypeRecord, - getContractFormatSchema, - getContributionSchema, - getFormsSchema, - getMapContext, - getMapSchema, - getRecordSchema, + Contract, + CONTRACT_MAX_BYTES, + InvalidDocument, + parseDescription, + parseImplementation, } from '../dist/index.js'; -const help = `MailSchema Registry tools +const help = `Mail Action Protocol 0.3 checks - mailschema check [--record] - mailschema schema [--record | --map | --context | --contract-format | --forms] + mailschema contract + mailschema description [--contract ] + mailschema implementation -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.`; +contract checks a type contract and prints its digest. description checks a +description and, with --contract, that it uses that contract as it allows. +implementation checks a Registry implementation record. Nothing is uploaded +or changed.`; -const schemas = { - record: getRecordSchema, - map: getMapSchema, - context: getMapContext, - 'contract-format': getContractFormatSchema, - forms: getFormsSchema, -}; +async function read(path) { + const info = await stat(path); + if (!info.isFile() || info.size > CONTRACT_MAX_BYTES) + throw new Error(`Expected a JSON file of at most ${CONTRACT_MAX_BYTES} bytes.`); + return new Uint8Array(await readFile(path)); +} + +function report(errors) { + if (!errors.length) return; + throw new InvalidDocument('Not valid.', errors); +} try { const { values, positionals } = parseArgs({ allowPositionals: true, - options: { - ...Object.fromEntries(Object.keys(schemas).map((name) => [name, { type: 'boolean' }])), - help: { type: 'boolean', short: 'h' }, - }, + options: { contract: { type: 'string' }, help: { type: 'boolean', short: 'h' } }, }); const [command, path, ...extra] = positionals; - 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((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 { - const info = await file.stat(); - if (!info.isFile() || info.size > 256 * 1024) - throw new Error('Expected a JSON file no larger than 256 KiB.'); - const bytes = Buffer.alloc(256 * 1024 + 1); - const { bytesRead } = await file.read(bytes, 0, bytes.length, 0); - if (bytesRead > 256 * 1024) throw new Error('JSON file exceeds 256 KiB.'); - value = JSON.parse(bytes.subarray(0, bytesRead).toString('utf8')); - } finally { - await file.close(); - } - if (values.record) assertTypeRecord(value); - else assertContribution(value); - console.log(`Valid MailSchema ${values.record ? 'type record' : 'contribution'}.`); + else if (!path || extra.length || (values.contract && command !== 'description')) + throw new Error(help); + else if (command === 'contract') { + const contract = Contract.parse(await read(path)); + console.log(`Valid type contract ${contract.id} ${contract.version}\n${contract.digest}`); + } else if (command === 'description') { + const description = parseDescription(await read(path)); + if (values.contract) + report(Contract.parse(await read(values.contract)).descriptionErrors(description)); + console.log(`Valid MAP 0.3 description ${description['@id']}`); + } else if (command === 'implementation') { + const record = parseImplementation(await read(path)); + console.log(`Valid implementation record for ${record.service}`); } else throw new Error(help); } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); + console.error(error instanceof InvalidDocument ? error.errors.join('\n') : error.message); process.exitCode = 1; } diff --git a/dist/context.jsonld b/dist/context.jsonld new file mode 100644 index 0000000..28c9635 --- /dev/null +++ b/dist/context.jsonld @@ -0,0 +1,50 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "@vocab": "https://mailschema.org/vocab/map/0.3#", + "MailAction": "https://mailschema.org/vocab/map/0.3#MailAction", + "profile": { + "@id": "profile", + "@type": "@id" + }, + "type": { + "@id": "type", + "@type": "@json" + }, + "service": { + "@id": "service", + "@type": "@json" + }, + "subject": { + "@id": "subject", + "@type": "@json" + }, + "terms": { + "@id": "terms", + "@type": "@json" + }, + "details": { + "@id": "details", + "@type": "@json" + }, + "operations": { + "@id": "operations", + "@type": "@json" + }, + "human": { + "@id": "human", + "@type": "@json" + }, + "recipient": "recipient", + "inLanguage": "inLanguage", + "issuedAt": { + "@id": "issuedAt", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expiresAt": { + "@id": "expiresAt", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + } +} diff --git a/dist/contract.schema.json b/dist/contract.schema.json new file mode 100644 index 0000000..b050127 --- /dev/null +++ b/dist/contract.schema.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "profile", + "name", + "summary", + "requirements", + "detailsSchema", + "operations" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.3" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "requirements": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "minItems": 1, + "maxItems": 64 + }, + "detailsSchema": { + "type": "object" + }, + "operations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "semantics", + "effects", + "bindings", + "actors", + "exactTerms", + "inputSchema", + "outcomes" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "semantics": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "effects": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "bindings": { + "type": "array", + "items": { + "enum": [ + "credential", + "capability" + ] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "actors": { + "type": "array", + "items": { + "enum": [ + "human", + "agent" + ] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "exactTerms": { + "type": "boolean" + }, + "inputSchema": { + "type": [ + "object", + "null" + ] + }, + "outcomes": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "capability": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "maxLifetimeSeconds" + ], + "properties": { + "kind": { + "enum": [ + "refusal", + "protective-report", + "address-confirmation" + ] + }, + "maxLifetimeSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 604800 + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "bindings": { + "contains": { + "const": "capability" + }, + "type": "array" + } + } + }, + "then": { + "required": [ + "capability" + ], + "properties": { + "inputSchema": { + "type": "null" + }, + "exactTerms": { + "const": true + } + } + }, + "else": { + "not": { + "required": [ + "capability" + ] + } + } + } + ] + }, + "minItems": 1, + "maxItems": 32 + } + } +} diff --git a/dist/contribution.schema.json b/dist/contribution.schema.json deleted file mode 100644 index 6b7742a..0000000 --- a/dist/contribution.schema.json +++ /dev/null @@ -1,874 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "MailSchema contribution format", - "description": "Repository contribution data. This is not a MAP message or wire representation.", - "$defs": { - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "slug": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "pattern": "\\S" - }, - "category": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "status": { - "enum": ["Draft", "Proposal", "Reuse assessment"] - }, - "version": { - "anyOf": [ - { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - { - "type": "null" - } - ] - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "overview": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "target": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "operations": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["id", "name", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "inputs": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "permissions": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "humanRoute": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "example": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "steps": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 1, - "maxItems": 100 - }, - "exception": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "steps", "exception"] - }, - "references": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "href", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "openQuestions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 0, - "maxItems": 100 - }, - "maintainers": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "pattern": "^/specification/(?:[a-z0-9-]+/)*$", - "maxLength": 300 - } - }, - "required": ["label", "href"] - } - }, - "required": [ - "slug", - "name", - "summary", - "category", - "status", - "version", - "profile", - "overview", - "target", - "operations", - "inputs", - "results", - "permissions", - "humanRoute", - "example", - "references", - "openQuestions", - "maintainers" - ], - "allOf": [ - { - "if": { - "properties": { - "status": { - "const": "Draft" - } - }, - "required": ["status"] - }, - "then": { - "required": ["definition"], - "properties": { - "version": { - "type": "string" - } - } - } - } - ] - }, - "record": { - "type": "object", - "additionalProperties": false, - "properties": { - "slug": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "pattern": "\\S" - }, - "category": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "status": { - "enum": ["Draft", "Proposal", "Reuse assessment"] - }, - "version": { - "anyOf": [ - { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - { - "type": "null" - } - ] - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "overview": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "target": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "operations": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["id", "name", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "inputs": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "permissions": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "humanRoute": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "example": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "steps": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 1, - "maxItems": 100 - }, - "exception": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "steps", "exception"] - }, - "references": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "href", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "openQuestions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 0, - "maxItems": 100 - }, - "maintainers": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "pattern": "^/specification/(?:[a-z0-9-]+/)*$", - "maxLength": 300 - } - }, - "required": ["label", "href"] - }, - "origin": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "contributors": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "history": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "contributionId": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - } - }, - "required": ["label", "description"] - }, - "minItems": 1, - "maxItems": 100 - } - }, - "required": [ - "slug", - "name", - "summary", - "category", - "status", - "version", - "profile", - "overview", - "target", - "operations", - "inputs", - "results", - "permissions", - "humanRoute", - "example", - "references", - "openQuestions", - "maintainers", - "origin", - "contributors", - "history" - ], - "allOf": [ - { - "if": { - "properties": { - "status": { - "const": "Draft" - } - }, - "required": ["status"] - }, - "then": { - "required": ["definition"], - "properties": { - "version": { - "type": "string" - } - } - } - } - ] - } - }, - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "new-type" - }, - "record": { - "$ref": "#/$defs/definition" - } - }, - "required": ["format", "id", "contributor", "summary", "kind", "record"] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "amendment" - }, - "baseDigest": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "record": { - "$ref": "#/$defs/definition" - } - }, - "required": ["format", "id", "contributor", "summary", "kind", "baseDigest", "record"] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "implementation" - }, - "type": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "typeVersion": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - "typeDigest": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "product": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "operations": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "minItems": 1, - "maxItems": 100 - }, - "evidence": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { - "enum": ["declaration", "test-report"] - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "reproduction": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["kind"], - "allOf": [ - { - "if": { - "properties": { - "kind": { - "const": "test-report" - } - }, - "required": ["kind"] - }, - "then": { - "required": ["url", "reproduction", "results"] - } - } - ] - } - }, - "required": [ - "format", - "id", - "contributor", - "summary", - "kind", - "type", - "typeVersion", - "typeDigest", - "profile", - "product", - "operations", - "evidence" - ] - } - ] -} diff --git a/dist/core.schema.json b/dist/core.schema.json new file mode 100644 index 0000000..b0ebbfc --- /dev/null +++ b/dist/core.schema.json @@ -0,0 +1,210 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "title": "MAP 0.3 description shape; semantic checks in Core are additionally required", + "type": "object", + "additionalProperties": false, + "required": [ + "@context", + "@type", + "@id", + "profile", + "type", + "issuedAt", + "expiresAt", + "inLanguage", + "service", + "recipient", + "subject", + "terms", + "details", + "operations", + "human" + ], + "properties": { + "@context": { + "const": "https://mailschema.org/contexts/map-0.3.jsonld" + }, + "@type": { + "const": "MailAction" + }, + "@id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.3" + }, + "type": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "contractDigest" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "contractDigest": { + "type": "string", + "minLength": 1, + "pattern": "^sha-256:[0-9a-f]{64}$" + } + } + }, + "issuedAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$" + }, + "expiresAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$" + }, + "inLanguage": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "service": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://(?![0-9.]+(:[0-9]+)?$)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(:[0-9]{1,5})?$", + "maxLength": 2048 + }, + "tenant": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + } + } + }, + "recipient": { + "type": "string", + "minLength": 1, + "maxLength": 320 + }, + "subject": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "terms": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "pattern": "^[ -~]+$", + "maxLength": 1024 + } + } + }, + "details": { + "type": "object" + }, + "operations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "capability": { + "type": "object", + "additionalProperties": false, + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://[^/?#@]+([/?][^#]*)?$", + "maxLength": 4096 + } + } + } + } + }, + "minItems": 1, + "maxItems": 32 + }, + "human": { + "type": "object", + "additionalProperties": false, + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://[^/?#@]+([/?][^#]*)?$", + "maxLength": 4096 + } + } + } + } +} diff --git a/dist/core/artifacts.d.ts b/dist/core/artifacts.d.ts index ff3c615..98d3993 100644 --- a/dist/core/artifacts.d.ts +++ b/dist/core/artifacts.d.ts @@ -1,36 +1,11 @@ -import { type ErrorObject, type ValidateFunction } from 'ajv/dist/2020.js'; -import type { JsonObject } from './types.ts'; -export declare const MAP_PROFILE = "https://mailschema.org/profiles/map/0.2"; -export declare const CORE_SCHEMA = "https://mailschema.org/schemas/map-0.2.schema.json"; -export declare const FORMS_SCHEMA = "https://mailschema.org/schemas/forms-0.1.schema.json"; -export declare const CONTRACT_FORMAT = "https://mailschema.org/schemas/type-contract-0.2.schema.json"; -export declare const CONTEXT = "https://mailschema.org/contexts/map-0.2.jsonld"; -export declare const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema"; -/** The schemas a contract may depend on without supplying them itself. */ -export declare const BUNDLED: ReadonlyMap; -/** A core or form fields definition by name, for the shared lexical vectors. */ -export declare const definition: (schema: string, name: string) => ValidateFunction; -/** Errors as `pointer message` lines, as the other MailSchema packages report them. */ -export declare function errorList(errors: ErrorObject[] | null | undefined, prefix?: string): string[]; -/** Checks a type contract against the contract format. */ -export declare const contractFormatErrors: (contract: unknown) => string[]; -/** Checks any MAP 0.2 description, request, result or problem against the core schema. */ -export declare const mapErrors: (document: unknown) => string[]; -/** - * Checks a description against the core description definition only. A description of a - * type the caller has no contract for can be checked this far. - */ -export declare const descriptionErrors: (description: unknown) => string[]; -/** Checks a request against the core request definition. Its identifier stays free if it fails. */ -export declare const requestErrors: (request: unknown) => string[]; -/** Checks a result against the core result definition only. */ -export declare const resultErrors: (result: unknown) => string[]; -/** Checks a problem against the core problem definition, including its type, status and code. */ -export declare const problemErrors: (problem: unknown) => string[]; -/** Whether a value is a request identifier: the core UUID URN form. Only one names a result resource. */ -export declare const isRequestId: (value: unknown) => boolean; -/** - * Whether a deadline, a core date-time plus `afterSeconds`, has been reached. Anything but - * a core date-time counts as reached, so a mistake fails closed. - */ -export declare const reached: (now: Date, deadline: string, afterSeconds?: number) => boolean; +export { coreSchema, contractSchema, implementationSchema } from './bundled.ts'; +export declare const PROFILE = "https://mailschema.org/profiles/map/0.3"; +export declare const CONTEXT = "https://mailschema.org/contexts/map-0.3.jsonld"; +/** The Content-Type of the MIME part that carries a description. */ +export declare const DESCRIPTION_MEDIA_TYPE = "application/ld+json; profile=\"https://mailschema.org/profiles/map/0.3\""; +export declare const DESCRIPTION_MAX_BYTES = 65536; +export declare const CONTRACT_MAX_BYTES = 262144; +/** The namespace of the initial effect vocabulary. */ +export declare const EFFECTS = "https://mailschema.org/effects/"; +/** The formats MAP schemas may assert. Every implementation checks exactly these. */ +export declare const FORMATS: readonly ["date", "date-time", "email", "uri"]; diff --git a/dist/core/artifacts.js b/dist/core/artifacts.js index f70cad5..619ce36 100644 --- a/dist/core/artifacts.js +++ b/dist/core/artifacts.js @@ -1,78 +1,30 @@ -// The bundled core artifacts and the validators built from them. +// MAP 0.3's identifiers, limits and schema validation. import Ajv2020 from 'ajv/dist/2020.js'; -import { contractFormat, coreSchema, formsSchema } from "./bundled.js"; -export const MAP_PROFILE = 'https://mailschema.org/profiles/map/0.2'; -export const CORE_SCHEMA = 'https://mailschema.org/schemas/map-0.2.schema.json'; -export const FORMS_SCHEMA = 'https://mailschema.org/schemas/forms-0.1.schema.json'; -export const CONTRACT_FORMAT = 'https://mailschema.org/schemas/type-contract-0.2.schema.json'; -export const CONTEXT = 'https://mailschema.org/contexts/map-0.2.jsonld'; -export const JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema'; -/** The schemas a contract may depend on without supplying them itself. */ -export const BUNDLED = new Map([ - [CORE_SCHEMA, coreSchema], - [FORMS_SCHEMA, formsSchema], -]); +import addFormats from 'ajv-formats'; +export { coreSchema, contractSchema, implementationSchema } from "./bundled.js"; +export const PROFILE = 'https://mailschema.org/profiles/map/0.3'; +export const CONTEXT = 'https://mailschema.org/contexts/map-0.3.jsonld'; +/** The Content-Type of the MIME part that carries a description. */ +export const DESCRIPTION_MEDIA_TYPE = `application/ld+json; profile="${PROFILE}"`; +export const DESCRIPTION_MAX_BYTES = 65536; +export const CONTRACT_MAX_BYTES = 262144; +/** The namespace of the initial effect vocabulary. */ +export const EFFECTS = 'https://mailschema.org/effects/'; +/** The formats MAP schemas may assert. Every implementation checks exactly these. */ +export const FORMATS = ['date', 'date-time', 'email', 'uri']; /** - * JSON Schema 2020-12 as MAP reads it. Lexical forms are the core schema's patterns, so - * `format` stays an annotation: format checkers differ between validators. References - * resolve only against schemas already added, never over the network. + * JSON Schema 2020-12 as MAP reads it: the listed formats asserted, and references resolved + * only within the schema, never over the network. MAP's own rules decide which keywords and + * formats a contract may use; the validator adds no checks of its own beyond the vocabulary. * @internal */ export function createValidator() { - const ajv = new Ajv2020({ - allErrors: true, - strict: true, - strictRequired: false, - validateFormats: false, - }); - // HTML autofill field names label form fields; they are annotations, not assertions. - ajv.addKeyword({ keyword: 'autocomplete', schemaType: 'string' }); - // Only JSON Schema 2020-12 vocabulary keywords. Ajv also reads OpenAPI's `nullable`, - // which admits a null other validators refuse, and the earlier `dependencies` and - // `definitions`; strict mode now refuses them as unknown. + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv, [...FORMATS]); + // Ajv also reads OpenAPI's `nullable` and the earlier `dependencies` and `definitions`. for (const keyword of ['nullable', 'dependencies', 'definitions']) ajv.removeKeyword(keyword); - for (const schema of BUNDLED.values()) - ajv.addSchema(schema); return ajv; } -const core = createValidator(); -/** A core or form fields definition by name, for the shared lexical vectors. */ -export const definition = (schema, name) => core.getSchema(`${schema}#/$defs/${name}`); -/** Errors as `pointer message` lines, as the other MailSchema packages report them. */ -export function errorList(errors, prefix = '') { - return (errors ?? []) - .slice(0, 100) - .map((error) => `${prefix + error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`); -} -const errorsOf = (validate, value) => validate(value) ? [] : errorList(validate.errors); -const DOCUMENT = core.getSchema(CORE_SCHEMA); -const DEFINITIONS = Object.fromEntries(['description', 'request', 'result', 'problem'].map((name) => [ - name, - definition(CORE_SCHEMA, name), -])); -const DATE_TIME = new RegExp(coreSchema.$defs.dateTime.pattern, 'u'); -const REQUEST_ID = definition(CORE_SCHEMA, 'uuidUrn'); -const CONTRACT = core.compile(contractFormat); -/** Checks a type contract against the contract format. */ -export const contractFormatErrors = (contract) => errorsOf(CONTRACT, contract); -/** Checks any MAP 0.2 description, request, result or problem against the core schema. */ -export const mapErrors = (document) => errorsOf(DOCUMENT, document); -/** - * Checks a description against the core description definition only. A description of a - * type the caller has no contract for can be checked this far. - */ -export const descriptionErrors = (description) => errorsOf(DEFINITIONS.description, description); -/** Checks a request against the core request definition. Its identifier stays free if it fails. */ -export const requestErrors = (request) => errorsOf(DEFINITIONS.request, request); -/** Checks a result against the core result definition only. */ -export const resultErrors = (result) => errorsOf(DEFINITIONS.result, result); -/** Checks a problem against the core problem definition, including its type, status and code. */ -export const problemErrors = (problem) => errorsOf(DEFINITIONS.problem, problem); -/** Whether a value is a request identifier: the core UUID URN form. Only one names a result resource. */ -export const isRequestId = (value) => REQUEST_ID(value); -/** - * Whether a deadline, a core date-time plus `afterSeconds`, has been reached. Anything but - * a core date-time counts as reached, so a mistake fails closed. - */ -export const reached = (now, deadline, afterSeconds = 0) => !(DATE_TIME.test(deadline) && now.getTime() < Date.parse(deadline) + afterSeconds * 1000); +/** Each validation error as its JSON Pointer and message. @internal */ +export const schemaErrors = (errors, prefix = '') => (errors ?? []).map((error) => `${prefix + error.instancePath || '/'}: ${error.message}`); diff --git a/dist/core/binding.d.ts b/dist/core/binding.d.ts deleted file mode 100644 index 52b8053..0000000 --- a/dist/core/binding.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { MapDescription } from './types.ts'; -/** The Content-Type of the part that carries a description, labelled with the profile. */ -export declare const DESCRIPTION_MEDIA_TYPE = "application/ld+json; profile=\"https://mailschema.org/profiles/map/0.2\""; -/** - * Whether a designated part carries a description of this profile: its media type is - * application/ld+json, compared case-insensitively, and its profile parameter lists the - * profile among its space-separated URIs. - */ -export declare const isDescriptionPart: (mediaType: string, profileParameter: string | undefined) => boolean; -/** The path of an absolute URL exactly as written, with no dot segments removed and nothing decoded. */ -export declare const writtenPath: (url: string) => string; -/** The possession capability: the last segment of the execution URL path, as written. */ -export declare const capability: (description: MapDescription) => string; -/** The rules a possession description's capability URLs meet when it is issued. */ -export declare function capabilityProblems(description: MapDescription): string[]; -/** - * Whether a Content-Type admits a request rather than a 415: the media type - * application/json, compared case-insensitively, with no parameter other than a UTF-8 - * charset. Only HTTP's optional whitespace, spaces and tabs, may surround each part, and - * empty parameter slots are ignored, as RFC 9110 permits. - */ -export declare function isJsonRequest(contentType: string | undefined): boolean; -/** The result resource of a request: the description's template with the identifier encoded. */ -export declare const resultUrl: (description: MapDescription, requestId: string) => string; diff --git a/dist/core/binding.js b/dist/core/binding.js deleted file mode 100644 index 512582d..0000000 --- a/dist/core/binding.js +++ /dev/null @@ -1,50 +0,0 @@ -// The HTTP and Structured Email bindings: capability URLs, the requests the execution URL -// admits, where results live and which message part carries a description. -import { MAP_PROFILE } from "./artifacts.js"; -/** The Content-Type of the part that carries a description, labelled with the profile. */ -export const DESCRIPTION_MEDIA_TYPE = `application/ld+json; profile="${MAP_PROFILE}"`; -/** - * Whether a designated part carries a description of this profile: its media type is - * application/ld+json, compared case-insensitively, and its profile parameter lists the - * profile among its space-separated URIs. - */ -export const isDescriptionPart = (mediaType, profileParameter) => mediaType.toLowerCase() === 'application/ld+json' && - (profileParameter ?? '').split(/[ \t]+/).includes(MAP_PROFILE); -/** The path of an absolute URL exactly as written, with no dot segments removed and nothing decoded. */ -export const writtenPath = (url) => /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*([^?#]*)/.exec(url)?.[1] ?? ''; -/** The possession capability: the last segment of the execution URL path, as written. */ -export const capability = (description) => writtenPath(description.service.execution.url).split('/').at(-1) ?? ''; -const UNGUESSABLE = /^[A-Za-z0-9_-]{22,}$/; -const dotSegment = (url) => writtenPath(url) - .split('/') - .some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment)); -/** The rules a possession description's capability URLs meet when it is issued. */ -export function capabilityProblems(description) { - const { url, resultUrlTemplate } = description.service.execution; - const issued = capability(description); - const problems = []; - if (!UNGUESSABLE.test(issued)) - problems.push('The capability is too short to be unguessable.'); - if ([url, resultUrlTemplate].some(dotSegment)) - problems.push('A capability URL must not contain dot segments.'); - if (!resultUrlTemplate.startsWith(`${url}/`)) - problems.push('The result template must extend the execution URL and its capability.'); - if (description.service.humanUrl.includes(issued)) - problems.push('The human route must not carry the capability.'); - return problems; -} -/** - * Whether a Content-Type admits a request rather than a 415: the media type - * application/json, compared case-insensitively, with no parameter other than a UTF-8 - * charset. Only HTTP's optional whitespace, spaces and tabs, may surround each part, and - * empty parameter slots are ignored, as RFC 9110 permits. - */ -export function isJsonRequest(contentType) { - const [type, ...parameters] = (contentType ?? '') - .split(';') - .map((part) => part.replace(/^[ \t]+|[ \t]+$/g, '').toLowerCase()); - return (type === 'application/json' && - parameters.every((parameter) => parameter === '' || /^charset=(?:utf-8|"utf-8")$/.test(parameter))); -} -/** The result resource of a request: the description's template with the identifier encoded. */ -export const resultUrl = (description, requestId) => description.service.execution.resultUrlTemplate.replace('{requestId}', () => encodeURIComponent(requestId)); diff --git a/dist/core/bundled.d.ts b/dist/core/bundled.d.ts index 2e0515d..548d9e2 100644 --- a/dist/core/bundled.d.ts +++ b/dist/core/bundled.d.ts @@ -1,4 +1,4 @@ -import coreSchema from '../map-0.2.schema.json'; -import formsSchema from '../forms-0.1.schema.json'; -import contractFormat from '../type-contract-0.2.schema.json'; -export { coreSchema, formsSchema, contractFormat }; +import coreSchema from '../core.schema.json'; +import contractSchema from '../contract.schema.json'; +import implementationSchema from '../implementation.schema.json'; +export { coreSchema, contractSchema, implementationSchema }; diff --git a/dist/core/bundled.js b/dist/core/bundled.js index ed3c526..90aca4a 100644 --- a/dist/core/bundled.js +++ b/dist/core/bundled.js @@ -1,7 +1,7 @@ -// The core artifacts, by their canonical files. Package preparation points these three -// imports at the package's own copies; no other line differs between the repository and -// the npm package. -import coreSchema from '../map-0.2.schema.json' with { type: 'json' }; -import formsSchema from '../forms-0.1.schema.json' with { type: 'json' }; -import contractFormat from '../type-contract-0.2.schema.json' with { type: 'json' }; -export { coreSchema, formsSchema, contractFormat }; +// The schemas the 0.3 profile record binds, by their canonical files. Package preparation +// points these three imports at the package's own copies; no other line differs between the +// repository and the npm package. +import coreSchema from '../core.schema.json' with { type: 'json' }; +import contractSchema from '../contract.schema.json' with { type: 'json' }; +import implementationSchema from '../implementation.schema.json' with { type: 'json' }; +export { coreSchema, contractSchema, implementationSchema }; diff --git a/dist/core/contract.d.ts b/dist/core/contract.d.ts index 7de079a..afd00c4 100644 --- a/dist/core/contract.d.ts +++ b/dist/core/contract.d.ts @@ -1,66 +1,69 @@ -import type { ProblemCode } from './documents.ts'; -import type { InputError, JsonObject, MapDescription, MapRequest, TypeContract, TypeReference } from './types.ts'; -/** A type contract that fails its own checks or its pinned digest. */ -export declare class InvalidContract extends Error { +import { type Description } from './description.ts'; +export interface Operation { + id: string; + name: string; + semantics: string; + effects: string[]; + bindings: ('credential' | 'capability')[]; + actors: ('human' | 'agent')[]; + exactTerms: boolean; + inputSchema: Record | null; + outcomes: Record; + capability?: { + kind: CapabilityKind; + maxLifetimeSeconds: number; + }; } -/** Reasons the core assigns to the approval lifecycle. */ -export declare const APPROVAL_REASONS: readonly ["declined", "stale-target", "expired", "superseded"]; -/** A MAP problem a request earns before the service's own state is consulted. */ -export type RequestProblem = { - code: ProblemCode; - title: string; - detail: string; +export interface ContractDocument { + id: string; + version: string; + profile: string; + name: string; + summary: string; + requirements: string[]; + detailsSchema: Record; + operations: Operation[]; +} +export type CapabilityKind = keyof typeof CAPABILITY_KINDS; +/** The capability binding's kinds: the exact effects each declares and its longest lifetime. */ +export declare const CAPABILITY_KINDS: { + readonly refusal: { + readonly effects: readonly ["refusal", "state"]; + readonly maxLifetimeSeconds: 604800; + }; + readonly 'protective-report': { + readonly effects: readonly ["protection", "state"]; + readonly maxLifetimeSeconds: 259200; + }; + readonly 'address-confirmation': { + readonly effects: readonly ["assertion", "state"]; + readonly maxLifetimeSeconds: 86400; + }; }; -/** - * A type contract an implementation vendors, verified against the digest it was pinned by - * and compiled once. It checks the descriptions, requests, inputs and results of that exact - * type. The Registry has already applied the contract rules, such as portable patterns, to - * the contract that digest names; loading refuses anything that would otherwise fail when a - * request arrives. - */ +/** Why the value is not a MAP 0.3 type contract, or no reasons. */ +export declare function contractErrors(value: unknown): string[]; +/** A valid contract with its digest and compiled schemas. */ export declare class Contract { #private; - readonly document: TypeContract; + readonly document: ContractDocument; + /** `sha-256:` and the SHA-256 of the contract's RFC 8785 form. */ readonly digest: string; - readonly requestSchema: JsonObject; - /** - * `digest` is the contract digest the implementation pinned; `dependencies` supplies, by - * URL, any pinned schema the package does not bundle. - */ - constructor(contract: unknown, requestSchema: unknown, { digest: pinned, dependencies, }: { - digest: string; - dependencies?: Record; - }); + /** The contract, or InvalidDocument with every reason the value is not one. */ + constructor(value: unknown); + /** The contract in the bytes or text, read as MAP JSON within the contract limit. */ + static parse(input: string | Uint8Array): Contract; get id(): string; get version(): string; - /** The type reference a description and a request of this contract name exactly. */ - get typeReference(): TypeReference; - operation(id: string): import("./types.ts").ContractOperation | undefined; - /** Whether completing the operation decides the interaction. */ - isDecision(id: string): boolean; - /** - * Every rule a description must satisfy beyond the core schema, for the service that issues - * it and the client that receives it. - */ - descriptionErrors(description: unknown): string[]; - /** - * The checks a service makes on a request once it has resolved the description it issued, - * compared the description digest and established the caller: the exact type, an offered - * operation the authority permits, and expiry. Undefined when they pass. The service then - * applies its own state (a decided interaction, a stale target) and `inputErrors`, in that - * order, before any effect. - */ - requestProblem(description: MapDescription, request: MapRequest, { now }: { - now: Date; - }): RequestProblem | undefined; + operation(id: string): Operation | undefined; + /** Why the details do not satisfy the contract's details schema, or no reasons. */ + detailsErrors(details: unknown): string[]; + /** Why the input is not acceptable for the operation, or no reasons. */ + inputErrors(operationId: string, input: unknown): string[]; /** - * Input problems for one request, each with a detail and a JSON Pointer into its input: the - * operation's input schema, then its field bindings against the description's details. Type - * rules a contract cannot express are the caller's. + * Why a valid description does not use this contract as it allows, or no reasons: it names + * this contract by identifier, version and digest; it offers only declared operations; a + * capability appears only where the contract permits one, at the service's origin and + * within the kind's lifetime; and its details satisfy the details schema. */ - inputErrors(description: MapDescription, request: MapRequest): InputError[]; - /** A request against the core request definition and this contract's request schema. */ - requestErrors(request: unknown): string[]; - /** The core result definition, then the output schema and reason the operation declares. */ - resultErrors(result: unknown): string[]; + descriptionErrors(description: Description): string[]; } diff --git a/dist/core/contract.js b/dist/core/contract.js index bff8a10..042be78 100644 --- a/dist/core/contract.js +++ b/dist/core/contract.js @@ -1,109 +1,193 @@ -import { BUNDLED, CORE_SCHEMA, JSON_SCHEMA_2020_12, MAP_PROFILE, contractFormatErrors, createValidator, descriptionErrors, errorList, reached, requestErrors, resultErrors, } from "./artifacts.js"; -import { capabilityProblems } from "./binding.js"; -import { digest } from "./document.js"; -import { inputError } from "./limits.js"; -import { escape, member } from "./pointer.js"; -import { fieldValuesSchema, formProblems, referenceProblem, values } from "./schemas.js"; -/** A type contract that fails its own checks or its pinned digest. */ -export class InvalidContract extends Error { +import { CONTRACT_MAX_BYTES, contractSchema, createValidator, EFFECTS, FORMATS, schemaErrors, } from "./artifacts.js"; +import { descriptionErrors } from "./description.js"; +import { digest, InvalidDocument, parse } from "./json.js"; +import { unportablePattern } from "./patterns.js"; +/** The capability binding's kinds: the exact effects each declares and its longest lifetime. */ +export const CAPABILITY_KINDS = { + refusal: { effects: ['refusal', 'state'], maxLifetimeSeconds: 604800 }, + 'protective-report': { effects: ['protection', 'state'], maxLifetimeSeconds: 259200 }, + 'address-confirmation': { effects: ['assertion', 'state'], maxLifetimeSeconds: 86400 }, +}; +// The JSON Schema 2020-12 keywords a contract's schemas may use. Every implementation +// enforces all of them alike; scope-resolved references and content annotations are absent. +const KEYWORDS = new Set([ + '$schema', '$ref', '$defs', '$comment', + 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas', + 'prefixItems', 'items', 'contains', 'properties', 'patternProperties', + 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', + 'type', 'enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', + 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', + 'uniqueItems', 'maxContains', 'minContains', 'maxProperties', 'minProperties', 'required', + 'dependentRequired', 'format', + 'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly', 'examples', +]); // prettier-ignore +const SCHEMA = ['additionalProperties', 'propertyNames', 'items', 'contains', 'not', 'if', 'then', 'else', 'unevaluatedItems', 'unevaluatedProperties']; // prettier-ignore +const SCHEMA_LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']; +const SCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'dependentSchemas']; +const DIALECT = 'https://json-schema.org/draft/2020-12/schema'; +// The keywords that apply their subschemas to the value itself, not to a member or item. +const IN_PLACE = new Set(['not', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'dependentSchemas']); +const escape = (name) => name.replaceAll('~', '~0').replaceAll('/', '~1'); +/** The values a schema object holds where schemas belong, as [keyword, pointer, value]. */ +function children(node, pointer) { + const found = []; + for (const keyword of SCHEMA) + if (Object.hasOwn(node, keyword)) + found.push([keyword, `${pointer}/${keyword}`, node[keyword]]); + for (const keyword of SCHEMA_LISTS) + if (Array.isArray(node[keyword])) + for (const [index, item] of node[keyword].entries()) + found.push([keyword, `${pointer}/${keyword}/${index}`, item]); + for (const keyword of SCHEMA_MAPS) { + const map = node[keyword]; + if (map && typeof map === 'object' && !Array.isArray(map)) + for (const [name, item] of Object.entries(map)) + found.push([keyword, `${pointer}/${keyword}/${escape(name)}`, item]); + } + return found; } -/** Reasons the core assigns to the approval lifecycle. */ -export const APPROVAL_REASONS = ['declined', 'stale-target', 'expired', 'superseded']; -const FIELD_VALIDATORS = 128; -function frozen(value) { - const copy = structuredClone(value); - const freeze = (item) => { - if (item && typeof item === 'object') { - Object.values(item).forEach(freeze); - Object.freeze(item); - } - }; - freeze(copy); - return copy; +const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value); +/** + * Every schema within a schema, object or boolean, with its JSON Pointer from the root, the + * schema itself first. + */ +function* schemas(schema, pointer) { + if (typeof schema === 'boolean') + yield [pointer, schema]; + if (!isObject(schema)) + return; + yield [pointer, schema]; + for (const [, at, item] of children(schema, pointer)) + yield* schemas(item, at); } -/** Each operation's branch of a request schema, with its input schema. */ -function branches(requestSchema) { - const last = (requestSchema.allOf ?? []).at(-1) ?? {}; - return (last.oneOf ?? [last]).map((branch) => { - const properties = (branch.properties ?? {}); - return { - operation: properties.operation?.const, - input: properties.input ?? {}, - }; - }); +/** + * Whether applying the schema can never finish: from its root, a chain of $ref and in-place + * keywords returns to a schema without moving into a member or item. + */ +function endless(schema) { + // Where each schema object sends its value, and whether it is the same value. + const links = new Map(); + for (const [at, node] of schemas(schema, '')) { + if (!isObject(node)) + continue; + const applied = children(node, at) + .filter(([keyword]) => keyword !== '$defs') + .map(([keyword, to]) => [to, IN_PLACE.has(keyword)]); + if (typeof node.$ref === 'string') + applied.push([node.$ref.slice(1), true]); + links.set(at, applied); + } + const reachable = ['']; + for (const at of reachable) + for (const [to] of links.get(at) ?? []) + if (!reachable.includes(to)) + reachable.push(to); + const state = new Map(); + const loops = (at) => { + if (state.has(at)) + return state.get(at) === 'open'; + state.set(at, 'open'); + const looped = (links.get(at) ?? []).some(([to, inPlace]) => inPlace && loops(to)); + state.set(at, 'done'); + return looped; + }; + return reachable.some(loops); } -/** A JSON Pointer to the failing member, naming a missing or extra member itself. */ -function pointerOf(error) { - const name = error.params.missingProperty ?? error.params.additionalProperty; - return name === undefined ? error.instancePath : `${error.instancePath}/${escape(String(name))}`; +/** Why a schema a contract supplies cannot be enforced alike everywhere, if it cannot. */ +function schemaProblems(schema, pointer) { + const problems = []; + const positions = new Set([...schemas(schema, '')].map(([at]) => at)); + for (const [at, value] of schemas(schema, pointer)) { + if (typeof value === 'boolean') + continue; + const node = value; + for (const keyword of Object.keys(node)) + if (!KEYWORDS.has(keyword)) + problems.push(`${at}: ${keyword} is not a MAP schema keyword`); + if ('$schema' in node && (at !== pointer || node.$schema !== DIALECT)) + problems.push(`${at}: $schema names JSON Schema 2020-12, at the schema's root only`); + const ref = node.$ref; + if (ref !== undefined && + (typeof ref !== 'string' || !ref.startsWith('#') || !positions.has(ref.slice(1)))) + problems.push(`${at}: $ref is a JSON Pointer to a schema within this schema`); + if ('format' in node && !FORMATS.includes(node.format)) + problems.push(`${at}: format is one of ${FORMATS.join(', ')}`); + if (typeof node.multipleOf === 'number' && !Number.isInteger(node.multipleOf)) + problems.push(`${at}: multipleOf is an integer`); + const patterns = [ + ...(typeof node.pattern === 'string' ? [node.pattern] : []), + ...Object.keys(node.patternProperties ?? {}), + ]; + for (const pattern of patterns) { + const problem = unportablePattern(pattern); + if (problem) + problems.push(`${at}: pattern ${JSON.stringify(pattern)} uses ${problem}`); + } + } + if (problems.length) + return problems; + if (endless(schema)) + return [`${pointer}: $ref leads back without moving into the value`]; + try { + createValidator().compile(schema); + } + catch (error) { + problems.push(`${pointer}: ${error.message}`); + } + return problems; } -/** Input errors as a detail and a pointer, each within the core limits, the first 100 kept. */ -function inputErrors(errors, prefix = '') { - const seen = new Set(); - return (errors ?? []) - .filter((error) => error.keyword !== 'if') - .map((error) => inputError(error.message ?? 'invalid', `${prefix}${pointerOf(error)}`)) - .filter(({ detail, pointer }) => { - const key = JSON.stringify([pointer, detail]); - return !seen.has(key) && Boolean(seen.add(key)); - }) - .slice(0, 100); +const validateContract = createValidator().compile(contractSchema); +/** An HTTPS URL's host and port as an origin compares them: lowercase, without :443. */ +const authority = (url) => /^https:\/\/([^/?#]*)/i.exec(url)?.[1].toLowerCase().replace(/:443$/, ''); +/** Why the value is not a MAP 0.3 type contract, or no reasons. */ +export function contractErrors(value) { + if (!validateContract(value)) + return schemaErrors(validateContract.errors); + const errors = []; + if (new TextEncoder().encode(JSON.stringify(value)).length > CONTRACT_MAX_BYTES) + errors.push(`/: exceeds ${CONTRACT_MAX_BYTES} bytes`); + const ids = value.operations.map((operation) => operation.id); + if (new Set(ids).size !== ids.length) + errors.push('/operations: identifiers must be unique'); + errors.push(...schemaProblems(value.detailsSchema, '/detailsSchema')); + for (const [index, operation] of value.operations.entries()) { + const at = `/operations/${index}`; + if (operation.inputSchema) + errors.push(...schemaProblems(operation.inputSchema, `${at}/inputSchema`)); + if (!operation.capability) + continue; + const kind = CAPABILITY_KINDS[operation.capability.kind]; + const expected = kind.effects.map((effect) => `${EFFECTS}${effect}`).sort(); + if (operation.effects.toSorted().join('\n') !== expected.join('\n')) + errors.push(`${at}/effects: a ${operation.capability.kind} capability declares exactly ${expected.join(' and ')}`); + if (operation.capability.maxLifetimeSeconds > kind.maxLifetimeSeconds) + errors.push(`${at}/capability/maxLifetimeSeconds: at most ${kind.maxLifetimeSeconds}`); + } + return errors; } -/** - * A type contract an implementation vendors, verified against the digest it was pinned by - * and compiled once. It checks the descriptions, requests, inputs and results of that exact - * type. The Registry has already applied the contract rules, such as portable patterns, to - * the contract that digest names; loading refuses anything that would otherwise fail when a - * request arrives. - */ +/** A valid contract with its digest and compiled schemas. */ export class Contract { document; + /** `sha-256:` and the SHA-256 of the contract's RFC 8785 form. */ digest; - requestSchema; - #ajv = createValidator(); #details; - #request; #inputs = new Map(); - #outputs = new Map(); - #fields = new Map(); - /** - * `digest` is the contract digest the implementation pinned; `dependencies` supplies, by - * URL, any pinned schema the package does not bundle. - */ - constructor(contract, requestSchema, { digest: pinned, dependencies = {}, }) { - const errors = contractFormatErrors(contract); + /** The contract, or InvalidDocument with every reason the value is not one. */ + constructor(value) { + const errors = contractErrors(value); if (errors.length) - throw new InvalidContract(`invalid type contract: ${errors.join('; ')}`); - this.document = frozen(contract); - if (this.document.profile !== MAP_PROFILE) - throw new InvalidContract('the contract is for another MAP profile'); + throw new InvalidDocument('The value is not a MAP 0.3 type contract.', errors); + this.document = structuredClone(value); this.digest = digest(this.document); - if (this.digest !== pinned) - throw new InvalidContract(`the contract digest is ${this.digest}, not the pinned ${pinned}`); - this.requestSchema = frozen(requestSchema); - const references = this.#pin(dependencies); - const supplied = [...references] - .filter(([url]) => !BUNDLED.has(url)) - .map(([, schema]) => schema); - this.#verify(references, supplied); - try { - // Every schema compiles now, so a pattern or reference fails the contract, not a request. - for (const schema of [...supplied, this.requestSchema]) - this.#ajv.addSchema(schema); - for (const schema of supplied) - this.#ajv.getSchema(String(schema.$id)); - this.#request = this.#ajv.getSchema(String(this.requestSchema.$id)); - if (this.document.detailsSchema) - this.#details = this.#ajv.compile(this.document.detailsSchema); - for (const branch of branches(this.requestSchema)) - this.#inputs.set(branch.operation, this.#ajv.compile(branch.input)); - for (const operation of this.document.operations) - for (const result of operation.results) - this.#outputs.set(`${operation.id}/${result.state}`, this.#ajv.compile(result.outputSchema)); - } - catch (error) { - throw new InvalidContract(`a schema does not compile: ${error.message}`); - } + const ajv = createValidator(); + this.#details = ajv.compile(this.document.detailsSchema); + for (const operation of this.document.operations) + if (operation.inputSchema) + this.#inputs.set(operation.id, ajv.compile(operation.inputSchema)); + } + /** The contract in the bytes or text, read as MAP JSON within the contract limit. */ + static parse(input) { + return new Contract(parse(input, CONTRACT_MAX_BYTES)); } get id() { return this.document.id; @@ -111,217 +195,59 @@ export class Contract { get version() { return this.document.version; } - /** The type reference a description and a request of this contract name exactly. */ - get typeReference() { - return { id: this.id, version: this.version, contractDigest: this.digest }; - } operation(id) { return this.document.operations.find((operation) => operation.id === id); } - /** Whether completing the operation decides the interaction. */ - isDecision(id) { - const found = this.operation(id); - if (!found) - throw new TypeError(`${id} is not an operation of this type.`); - return !found.repeatable; - } - /** - * Every rule a description must satisfy beyond the core schema, for the service that issues - * it and the client that receives it. - */ - descriptionErrors(description) { - const errors = descriptionErrors(description); - if (errors.length) - return errors; - const issued = description; - if (!this.#names(issued.type)) - return ['The description names another type contract.']; - const problems = [...this.#detailsProblems(issued), ...this.#operationProblems(issued)]; - if (!(Date.parse(issued.describedAt) < Date.parse(issued.expiresAt))) - problems.push('The interaction expires before it was described.'); - if (issued.service.authority === 'possession') - problems.push(...capabilityProblems(issued)); - return problems; - } - /** - * The checks a service makes on a request once it has resolved the description it issued, - * compared the description digest and established the caller: the exact type, an offered - * operation the authority permits, and expiry. Undefined when they pass. The service then - * applies its own state (a decided interaction, a stale target) and `inputErrors`, in that - * order, before any effect. - */ - requestProblem(description, request, { now }) { - if (!this.#names(request.type) || !this.#names(description.type)) - return { - code: 'unsupported-type', - title: 'Unsupported interaction type', - detail: 'The service does not implement this exact interaction type contract.', - }; - if (!this.#offered(description, request.operation)) - return { - code: 'unsupported-operation', - title: 'Unsupported operation', - detail: 'The operation was not offered in this interaction.', - }; - if (reached(now, description.expiresAt)) - return { - code: 'expired-interaction', - title: 'Interaction expired', - detail: 'The interaction expired before the request was processed.', - }; - } - /** - * Input problems for one request, each with a detail and a JSON Pointer into its input: the - * operation's input schema, then its field bindings against the description's details. Type - * rules a contract cannot express are the caller's. - */ - inputErrors(description, request) { - const found = this.operation(request.operation); - const validate = this.#inputs.get(request.operation); - if (!found || !validate) - return [inputError('The operation is not part of this type.', '')]; - if (!validate(request.input)) - return inputErrors(validate.errors); - return (found.fieldBindings ?? []) - .flatMap((binding) => { - const fields = member(description.details, binding.fields); - const supplied = member(request.input, binding.input); - if (!fields) - return supplied === undefined - ? [] - : [inputError('This interaction defines no fields for these values.', binding.input)]; - if (supplied === undefined) - return (fields.required ?? []).length - ? [inputError('Values for the required fields are missing.', binding.input)] - : []; - const schema = this.#fieldValues(fields); - return schema(supplied) ? [] : inputErrors(schema.errors, binding.input); - }) - .slice(0, 100); - } - /** A request against the core request definition and this contract's request schema. */ - requestErrors(request) { - return [ - ...requestErrors(request), - ...(this.#request(request) ? [] : errorList(this.#request.errors)), - ]; - } - /** The core result definition, then the output schema and reason the operation declares. */ - resultErrors(result) { - const errors = resultErrors(result); - if (errors.length) - return errors; - const recorded = result; - if (!this.#names(recorded.type)) - return ['The result names another type contract.']; - const found = this.operation(recorded.operation); - if (!found) - return [`${recorded.operation} is not an operation of this type.`]; - const declared = found.results.find((entry) => entry.state === recorded.state); - if (!declared) - return [`${found.id} does not declare the state ${recorded.state}.`]; - const validate = this.#outputs.get(`${found.id}/${declared.state}`); - const problems = validate(recorded.output) ? [] : errorList(validate.errors, '/output'); - if (recorded.state === 'failed' && !declared.reasons?.includes(recorded.reason)) - problems.push(`${found.id} does not declare the reason ${recorded.reason}.`); - return problems; - } - #names(type) { - return (type.id === this.id && type.version === this.version && type.contractDigest === this.digest); - } - #offered(description, id) { - return (description.operations.some((offered) => offered.id === id) && - Boolean(this.operation(id)?.authority.includes(description.service.authority))); - } - #operationProblems(description) { - const ids = description.operations.map((offered) => offered.id); - const authority = description.service.authority; - return [ - ...(new Set(ids).size === ids.length ? [] : ['An operation is offered twice.']), - ...ids - .filter((id) => !this.operation(id)?.authority.includes(authority)) - .map((id) => `${id} is not a ${authority} operation of this type.`), - ]; + /** Why the details do not satisfy the contract's details schema, or no reasons. */ + detailsErrors(details) { + return this.#details(details) ? [] : schemaErrors(this.#details.errors, '/details'); } - #detailsProblems(description) { - const present = Object.hasOwn(description, 'details'); - const valid = this.#details ? present && this.#details(description.details) : !present; - if (!valid) - return ['The details do not satisfy the type contract.']; - const blocks = new Set(this.document.operations.flatMap((operation) => (operation.fieldBindings ?? []).map((binding) => binding.fields))); - return [...blocks].flatMap((pointer) => { - const fields = member(description.details, pointer); - return fields ? formProblems(fields).map((problem) => `${pointer}: ${problem}`) : []; - }); + /** Why the input is not acceptable for the operation, or no reasons. */ + inputErrors(operationId, input) { + const operation = this.operation(operationId); + if (!operation) + return [`/operation: ${operationId} is not an operation of this contract`]; + const validate = this.#inputs.get(operationId); + if (!validate) + return input === undefined ? [] : ['/input: this operation accepts no input']; + return validate(input) ? [] : schemaErrors(validate.errors, '/input'); } /** - * The compiled schema of a fields block's values. Least recently used blocks leave first, - * with their compiled copies, so a long-running service stays bounded. + * Why a valid description does not use this contract as it allows, or no reasons: it names + * this contract by identifier, version and digest; it offers only declared operations; a + * capability appears only where the contract permits one, at the service's origin and + * within the kind's lifetime; and its details satisfy the details schema. */ - #fieldValues(fields) { - const key = digest(fields); - let entry = this.#fields.get(key); - if (entry) - this.#fields.delete(key); - else { - const schema = fieldValuesSchema(fields); - entry = { schema, validate: this.#ajv.compile(schema) }; - } - this.#fields.set(key, entry); - if (this.#fields.size > FIELD_VALIDATORS) { - const [oldest, evicted] = this.#fields.entries().next().value; - this.#fields.delete(oldest); - this.#ajv.removeSchema(evicted.schema); - } - return entry.validate; - } - /** The pinned schemas by URL, bundled or supplied, each matching its pinned digest. */ - #pin(dependencies) { - const references = new Map(); - for (const dependency of this.document.dependencies ?? []) { - const schema = (BUNDLED.get(dependency.url) ?? dependencies[dependency.url]); - if (!schema) - throw new InvalidContract(`unknown dependency ${dependency.url}`); - if (digest(schema) !== dependency.canonicalDigest) - throw new InvalidContract(`the pinned digest of ${dependency.url} differs`); - if (schema.$id !== dependency.url) - throw new InvalidContract(`the schema pinned as ${dependency.url} has another $id`); - references.set(dependency.url, BUNDLED.has(dependency.url) ? schema : frozen(schema)); - } - if (!references.has(CORE_SCHEMA)) - throw new InvalidContract('the core schema must be pinned'); - return references; - } - #verify(references, supplied) { - const inline = [ - this.document.detailsSchema, - ...this.document.operations.flatMap((operation) => operation.results.map((result) => result.outputSchema)), - ].filter(Boolean); - // Every schema is valid JSON Schema 2020-12, so no malformed keyword fails at request time. - for (const schema of [...inline, this.requestSchema, ...supplied]) - if (!this.#ajv.validateSchema(schema)) - throw new InvalidContract(`a schema is not valid JSON Schema 2020-12: ${this.#ajv.errorsText(this.#ajv.errors)}`); - const reference = this.document.requestSchema; - if (this.requestSchema.$schema !== JSON_SCHEMA_2020_12) - throw new InvalidContract('the request schema must declare JSON Schema 2020-12'); - if (this.requestSchema.$id !== reference.url) - throw new InvalidContract('the request schema $id differs from the contract'); - if (digest(this.requestSchema) !== reference.canonicalDigest) - throw new InvalidContract('the request schema digest differs from the contract'); - const constants = new Set(values(this.requestSchema, 'const')); - const ids = this.document.operations.map((operation) => operation.id); - for (const expected of [this.id, this.version, ...ids]) - if (!constants.has(expected)) - throw new InvalidContract(`the request schema does not bind ${expected}`); - if (new Set(ids).size !== ids.length) - throw new InvalidContract('duplicate operation identifiers'); - for (const operation of this.document.operations) { - const states = operation.results.map((result) => result.state); - if (new Set(states).size !== states.length) - throw new InvalidContract(`duplicate ${operation.id} result states`); + descriptionErrors(description) { + const invalid = descriptionErrors(description); + if (invalid.length) + return invalid; + const errors = []; + const { type } = description; + if (type.id !== this.id || type.version !== this.version) + errors.push(`/type: names ${type.id} ${type.version}, not this contract`); + if (type.contractDigest !== this.digest) + errors.push('/type/contractDigest: does not match'); + if (description.profile !== this.document.profile) + errors.push('/profile: differs from the contract profile'); + const lifetime = (Date.parse(description.expiresAt) - Date.parse(description.issuedAt)) / 1000; + const origin = authority(description.service.id); + for (const [index, offered] of description.operations.entries()) { + const at = `/operations/${index}`; + const operation = this.operation(offered.id); + if (!operation) { + errors.push(`${at}/id: ${offered.id} is not an operation of this contract`); + continue; + } + if (!offered.capability) + continue; + if (!operation.capability) + errors.push(`${at}/capability: the contract permits no capability for ${offered.id}`); + else if (lifetime > operation.capability.maxLifetimeSeconds) + errors.push(`/expiresAt: a ${offered.id} capability lasts at most ${operation.capability.maxLifetimeSeconds} seconds`); + if (authority(offered.capability.url) !== origin) + errors.push(`${at}/capability/url: must have the service's origin`); } - const problem = referenceProblem(inline, this.requestSchema, references, supplied); - if (problem) - throw new InvalidContract(problem); + return [...errors, ...this.detailsErrors(description.details)]; } } diff --git a/dist/core/description.d.ts b/dist/core/description.d.ts new file mode 100644 index 0000000..69df5dc --- /dev/null +++ b/dist/core/description.d.ts @@ -0,0 +1,45 @@ +export interface Description { + '@context': string; + '@type': 'MailAction'; + '@id': string; + profile: string; + type: { + id: string; + version: string; + contractDigest: string; + }; + issuedAt: string; + expiresAt: string; + inLanguage: string; + service: { + id: string; + tenant?: string; + }; + recipient: string; + subject: { + id: string; + title: string; + }; + terms: { + id: string; + version: string; + }; + details: Record; + operations: { + id: string; + capability?: { + url: string; + }; + }[]; + human: { + url: string; + }; +} +/** + * Why the value is not a MAP 0.3 description, or no reasons. Beyond the core schema: + * expiry follows issuance, operation identifiers are unique, and no security-sensitive + * identifier contains whitespace, a control or a formatting character. + */ +export declare function descriptionErrors(value: unknown): string[]; +/** The description in the bytes or text, or InvalidDocument with every reason it is not one. */ +export declare function parseDescription(input: string | Uint8Array): Description; diff --git a/dist/core/description.js b/dist/core/description.js new file mode 100644 index 0000000..93838e9 --- /dev/null +++ b/dist/core/description.js @@ -0,0 +1,47 @@ +// A MAP description: the JSON-LD a message carries beside its readable content. +import { coreSchema, createValidator, DESCRIPTION_MAX_BYTES, schemaErrors } from "./artifacts.js"; +import { InvalidDocument, parse } from "./json.js"; +const validate = createValidator().compile(coreSchema); +// Whitespace, controls, bidirectional and invisible formatting characters. +const UNSAFE = /[\p{White_Space}\p{Cc}\p{Cf}]/u; +/** + * Why the value is not a MAP 0.3 description, or no reasons. Beyond the core schema: + * expiry follows issuance, operation identifiers are unique, and no security-sensitive + * identifier contains whitespace, a control or a formatting character. + */ +export function descriptionErrors(value) { + if (!validate(value)) + return schemaErrors(validate.errors); + const errors = []; + if (Date.parse(value.expiresAt) <= Date.parse(value.issuedAt)) + errors.push('/expiresAt: must be later than issuedAt'); + const ids = value.operations.map((operation) => operation.id); + if (new Set(ids).size !== ids.length) + errors.push('/operations: identifiers must be unique'); + const identifiers = [ + ['/@id', value['@id']], + ['/type/id', value.type.id], + ['/service/id', value.service.id], + ['/service/tenant', value.service.tenant], + ['/recipient', value.recipient], + ['/subject/id', value.subject.id], + ['/terms/id', value.terms.id], + ['/human/url', value.human.url], + ...value.operations.map((operation, index) => [ + `/operations/${index}/capability/url`, + operation.capability?.url, + ]), + ]; + for (const [pointer, identifier] of identifiers) + if (identifier !== undefined && UNSAFE.test(identifier)) + errors.push(`${pointer}: must not contain whitespace, controls or formatting characters`); + return errors; +} +/** The description in the bytes or text, or InvalidDocument with every reason it is not one. */ +export function parseDescription(input) { + const value = parse(input, DESCRIPTION_MAX_BYTES); + const errors = descriptionErrors(value); + if (errors.length) + throw new InvalidDocument('The value is not a MAP 0.3 description.', errors); + return value; +} diff --git a/dist/core/document.d.ts b/dist/core/document.d.ts deleted file mode 100644 index a90e886..0000000 --- a/dist/core/document.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** A MAP document that is not I-JSON within the core limits. */ -export declare class InvalidDocument extends Error { -} -export declare const MAX_BYTES: number; -export declare const MAX_DEPTH = 32; -/** - * Parse a MAP document as I-JSON (RFC 7493) within the core limits: valid UTF-8 with no - * byte order mark, at most 64 KiB, no duplicate member names, no lone surrogates, - * noncharacters or U+0000 in any string, every number within ±(2^53−1), and arrays and - * objects nested at most 32 deep, the outermost counting as one. Text is measured in its - * UTF-8 bytes; bytes must be strict UTF-8. - */ -export declare function parse(input: string | Uint8Array): unknown; -/** - * The RFC 8785 canonical form of a JSON value: members sorted by their UTF-16 code units, - * ECMAScript's minimal string escapes and its number format. An undefined member is - * left out and an undefined array item is null, as JSON.stringify writes them. - */ -export declare function canonicalize(value: unknown): string; -/** - * `sha-256:` and the SHA-256 of the RFC 8785 canonical form: the digest MAP uses for - * descriptions, contracts and pinned schemas. - */ -export declare const digest: (value: unknown) => string; diff --git a/dist/core/documents.d.ts b/dist/core/documents.d.ts deleted file mode 100644 index 1bef34f..0000000 --- a/dist/core/documents.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { InputError, JsonObject, MapDescription, MapProblem, MapRequest, MapResult, ResultState, Target } from './types.ts'; -/** The HTTP status of every MAP problem code, as the core schema fixes it. */ -export declare const PROBLEM_STATUS: { - readonly 'invalid-request': 400; - readonly 'authentication-required': 401; - readonly refused: 403; - readonly 'result-not-found': 404; - readonly 'stale-target': 409; - readonly 'idempotency-conflict': 409; - readonly 'request-in-progress': 409; - readonly 'already-decided': 409; - readonly 'expired-interaction': 410; - readonly 'unsupported-type': 422; - readonly 'unsupported-operation': 422; -}; -export type ProblemCode = keyof typeof PROBLEM_STATUS; -/** - * A result for a request, with every correlation member. `approvalUrl` is given exactly for - * approval-required and `reason` exactly for failed. Like every builder here, it throws - * rather than return a document the core refuses. - */ -export declare function result(request: MapRequest, { state, target, resultUrl, recordedAt, output, reason, approvalUrl, actor, }: { - state: ResultState; - target: Target; - resultUrl: string; - recordedAt: Date; - output?: JsonObject; - reason?: string; - approvalUrl?: string; - actor?: string; -}): MapResult; -/** - * The next state of a pending or approval-required result. Every correlation member and the - * actor stay; the state, reason, output and recording time change. - */ -export declare function transition(previous: MapResult, { state, recordedAt, reason, output, }: { - state: ResultState; - recordedAt: Date; - reason?: string; - output?: JsonObject; -}): MapResult; -/** - * A problem. With `requestId` it is correlated and carries `instance`, `profile`, - * `requestId`, `interactionId` and `code`; without, it is plain RFC 9457. It stays within - * the core limits: a long detail is cut, each input error is bounded, and only as many of - * the first 100 errors as fit within 64 KiB are kept. - */ -export declare function problem(code: ProblemCode, { title, detail, requestId, interactionId, resultUrl, target, errors, }: { - title: string; - detail: string; - requestId?: string; - interactionId?: string; - resultUrl?: string; - target?: Target; - errors?: InputError[]; -}): MapProblem; -/** The HTTP status of a result: 202 while work is pending, otherwise 200. */ -export declare const resultStatus: (document: MapResult) => 200 | 202; -/** - * Results stay retrievable until the later of the interaction's expiry and the retention - * interval measured from the latest recorded state. - */ -export declare const retainUntil: (description: MapDescription, recordedAt: Date) => Date; -/** - * An undecided approval ends as failed, with reason expired, at the interaction's expiry, - * whether or not anyone looks. The settled result, or undefined when nothing changes. - */ -export declare function settle(document: MapResult, description: MapDescription, now: Date): MapResult | undefined; diff --git a/dist/core/documents.js b/dist/core/documents.js deleted file mode 100644 index e1d43fc..0000000 --- a/dist/core/documents.js +++ /dev/null @@ -1,145 +0,0 @@ -// Building MAP results and problems, and the lifecycle rules every service applies. -import { MAP_PROFILE, problemErrors, reached, resultErrors } from "./artifacts.js"; -import { problemMembers, withinDocument } from "./limits.js"; -/** The HTTP status of every MAP problem code, as the core schema fixes it. */ -export const PROBLEM_STATUS = { - 'invalid-request': 400, - 'authentication-required': 401, - refused: 403, - 'result-not-found': 404, - 'stale-target': 409, - 'idempotency-conflict': 409, - 'request-in-progress': 409, - 'already-decided': 409, - 'expired-interaction': 410, - 'unsupported-type': 422, - 'unsupported-operation': 422, -}; -const PROBLEM_TYPES = 'https://mailschema.org/problems/'; -const NON_TERMINAL = ['pending', 'approval-required']; -/** The document, once the core definition `check` names accepts it. */ -function built(document, check) { - const refused = check(document); - if (refused.length) - throw new TypeError(`The core refuses this document: ${refused.slice(0, 3).join('; ')}`); - return document; -} -const defined = (document) => Object.fromEntries(Object.entries(document).filter(([, value]) => value !== undefined)); -/** A result with its members in one order, whichever builder made it, checked by the core. */ -const resultDocument = (members) => built(defined({ - kind: 'MapResult', - profile: MAP_PROFILE, - requestId: members.requestId, - interactionId: members.interactionId, - descriptionDigest: members.descriptionDigest, - type: members.type, - operation: members.operation, - state: members.state, - target: members.target, - recordedAt: members.recordedAt, - resultUrl: members.resultUrl, - actor: members.actor, - approvalUrl: members.approvalUrl, - reason: members.reason, - output: members.output, -}), resultErrors); -/** - * A result for a request, with every correlation member. `approvalUrl` is given exactly for - * approval-required and `reason` exactly for failed. Like every builder here, it throws - * rather than return a document the core refuses. - */ -export function result(request, { state, target, resultUrl, recordedAt, output = {}, reason, approvalUrl, actor, }) { - if ((state === 'approval-required') !== (approvalUrl !== undefined)) - throw new TypeError('approvalUrl is given exactly when the state is approval-required.'); - if ((state === 'failed') !== (reason !== undefined)) - throw new TypeError('reason is given exactly when the state is failed.'); - return resultDocument({ - requestId: request.requestId, - interactionId: request.interactionId, - descriptionDigest: request.descriptionDigest, - type: request.type, - operation: request.operation, - state, - target, - recordedAt: recordedAt.toISOString(), - resultUrl, - actor, - approvalUrl, - reason, - output, - }); -} -/** - * The next state of a pending or approval-required result. Every correlation member and the - * actor stay; the state, reason, output and recording time change. - */ -export function transition(previous, { state, recordedAt, reason, output = {}, }) { - if (!NON_TERMINAL.includes(previous.state)) - throw new TypeError(`A ${previous.state} result is terminal.`); - if (NON_TERMINAL.includes(state)) - throw new TypeError('A transition ends pending work.'); - if ((state === 'failed') !== (reason !== undefined)) - throw new TypeError('reason is given exactly when the state is failed.'); - return resultDocument({ - ...previous, - approvalUrl: undefined, - state, - reason, - output, - recordedAt: recordedAt.toISOString(), - }); -} -/** - * A problem. With `requestId` it is correlated and carries `instance`, `profile`, - * `requestId`, `interactionId` and `code`; without, it is plain RFC 9457. It stays within - * the core limits: a long detail is cut, each input error is bounded, and only as many of - * the first 100 errors as fit within 64 KiB are kept. - */ -export function problem(code, { title, detail, requestId, interactionId, resultUrl, target, errors, }) { - const members = problemMembers(title, detail, errors); - if (interactionId && !requestId) - throw new TypeError('An interaction is named only with its request.'); - if (requestId && code === 'authentication-required') - throw new TypeError('authentication-required is never correlated.'); - if (members.errors && !(requestId && code === 'invalid-request')) - throw new TypeError('Errors belong to a correlated invalid-request.'); - let correlation = {}; - if (requestId) { - if (!resultUrl) - throw new TypeError('A correlated problem names its result URL.'); - if (!interactionId && code !== 'result-not-found') - throw new TypeError('A correlated problem names its interaction.'); - if (code === 'stale-target' && !target) - throw new TypeError('A stale-target problem carries the current target.'); - correlation = { instance: resultUrl, profile: MAP_PROFILE, requestId, interactionId, code }; - } - return built(withinDocument(defined({ - type: `${PROBLEM_TYPES}${code}`, - title: members.title, - status: PROBLEM_STATUS[code], - detail: members.detail, - ...correlation, - target, - errors: members.errors, - })), problemErrors); -} -/** The HTTP status of a result: 202 while work is pending, otherwise 200. */ -export const resultStatus = (document) => NON_TERMINAL.includes(document.state) ? 202 : 200; -/** - * Results stay retrievable until the later of the interaction's expiry and the retention - * interval measured from the latest recorded state. - */ -export const retainUntil = (description, recordedAt) => new Date(Math.max(Date.parse(description.expiresAt), recordedAt.getTime() + description.service.execution.resultRetentionSeconds * 1000)); -/** - * An undecided approval ends as failed, with reason expired, at the interaction's expiry, - * whether or not anyone looks. The settled result, or undefined when nothing changes. - */ -export function settle(document, description, now) { - if (document.state !== 'approval-required' || !reached(now, description.expiresAt)) - return; - return transition(document, { - state: 'failed', - reason: 'expired', - recordedAt: new Date(description.expiresAt), - }); -} diff --git a/dist/core/implementation.d.ts b/dist/core/implementation.d.ts new file mode 100644 index 0000000..05fefde --- /dev/null +++ b/dist/core/implementation.d.ts @@ -0,0 +1,32 @@ +export interface ImplementationRecord { + service: string; + maintainer: { + name: string; + url: string; + }; + type: { + id: string; + version: string; + contractDigest: string; + }; + operations: string[]; + binding: string; + artifact?: { + url: string; + digest: string; + digestMode: 'canonical-json' | 'bytes'; + }; + format?: string; + status: 'Draft' | 'Experimental' | 'Stable' | 'Deprecated' | 'Superseded'; + documentation: string; + evidence: { + kind: 'declaration' | 'test-report'; + url: string; + summary: string; + }[]; + successor?: string; +} +/** Why the value is not an implementation record, or no reasons. */ +export declare function implementationErrors(value: unknown): string[]; +/** The record in the bytes or text, or InvalidDocument with every reason it is not one. */ +export declare function parseImplementation(input: string | Uint8Array): ImplementationRecord; diff --git a/dist/core/implementation.js b/dist/core/implementation.js new file mode 100644 index 0000000..646781d --- /dev/null +++ b/dist/core/implementation.js @@ -0,0 +1,25 @@ +// An implementation record: a Registry declaration or report of a service's support for exact +// contract versions. +import { CONTRACT_MAX_BYTES, createValidator, implementationSchema, schemaErrors, } from "./artifacts.js"; +import { InvalidDocument, parse } from "./json.js"; +const validate = createValidator().compile(implementationSchema); +/** Why the value is not an implementation record, or no reasons. */ +export function implementationErrors(value) { + if (!validate(value)) + return schemaErrors(validate.errors); + const errors = []; + const { origin } = new URL(value.service); + if (value.service !== origin && value.service !== `${origin}/`) + errors.push('/service: must be an HTTPS origin, not an operation path'); + if (new Set(value.operations).size !== value.operations.length) + errors.push('/operations: identifiers must be unique'); + return errors; +} +/** The record in the bytes or text, or InvalidDocument with every reason it is not one. */ +export function parseImplementation(input) { + const value = parse(input, CONTRACT_MAX_BYTES); + const errors = implementationErrors(value); + if (errors.length) + throw new InvalidDocument('The value is not an implementation record.', errors); + return value; +} diff --git a/dist/core/index.d.ts b/dist/core/index.d.ts index d7779eb..2b64971 100644 --- a/dist/core/index.d.ts +++ b/dist/core/index.d.ts @@ -1,6 +1,7 @@ -export { MAP_PROFILE, CORE_SCHEMA, FORMS_SCHEMA, CONTRACT_FORMAT, CONTEXT, mapErrors, descriptionErrors, requestErrors, resultErrors, problemErrors, isRequestId, reached, } from './artifacts.ts'; -export { InvalidDocument, parse, canonicalize, digest } from './document.ts'; -export { APPROVAL_REASONS, Contract, InvalidContract, type RequestProblem } from './contract.ts'; -export { PROBLEM_STATUS, type ProblemCode, result, transition, problem, resultStatus, retainUntil, settle, } from './documents.ts'; -export { DESCRIPTION_MEDIA_TYPE, isDescriptionPart, capability, writtenPath, isJsonRequest, resultUrl, } from './binding.ts'; -export type { Authority, Consequence, ContractOperation, InputError, JsonObject, MapDescription, MapProblem, MapRequest, MapResult, ResultState, SchemaReference, Target, TypeContract, TypeReference, } from './types.ts'; +export { PROFILE, CONTEXT, DESCRIPTION_MEDIA_TYPE, DESCRIPTION_MAX_BYTES, CONTRACT_MAX_BYTES, EFFECTS, FORMATS, coreSchema, contractSchema, implementationSchema, } from './artifacts.ts'; +export { InvalidDocument, MAX_DEPTH, decodeUtf8, parse, canonicalize, digest } from './json.ts'; +export { sha256 } from './sha256.ts'; +export { descriptionErrors, parseDescription, type Description } from './description.ts'; +export { CAPABILITY_KINDS, Contract, contractErrors, type CapabilityKind, type ContractDocument, type Operation, } from './contract.ts'; +export { implementationErrors, parseImplementation, type ImplementationRecord, } from './implementation.ts'; +export { unportablePattern } from './patterns.ts'; diff --git a/dist/core/index.js b/dist/core/index.js index 65c9431..427d5b2 100644 --- a/dist/core/index.js +++ b/dist/core/index.js @@ -1,9 +1,9 @@ -// Mail Action Protocol 0.2 processing. It parses, canonicalizes, digests and validates MAP -// documents, checks the type contracts an implementation vendors, and builds results and -// problems. It does not establish endpoint trust, verify email authentication, grant -// authority or send email. -export { MAP_PROFILE, CORE_SCHEMA, FORMS_SCHEMA, CONTRACT_FORMAT, CONTEXT, mapErrors, descriptionErrors, requestErrors, resultErrors, problemErrors, isRequestId, reached, } from "./artifacts.js"; -export { InvalidDocument, parse, canonicalize, digest } from "./document.js"; -export { APPROVAL_REASONS, Contract, InvalidContract } from "./contract.js"; -export { PROBLEM_STATUS, result, transition, problem, resultStatus, retainUntil, settle, } from "./documents.js"; -export { DESCRIPTION_MEDIA_TYPE, isDescriptionPart, capability, writtenPath, isJsonRequest, resultUrl, } from "./binding.js"; +// Mail Action Protocol 0.3 processing: one implementation, run unchanged by the repository's +// site and checks and shipped as the npm package. +export { PROFILE, CONTEXT, DESCRIPTION_MEDIA_TYPE, DESCRIPTION_MAX_BYTES, CONTRACT_MAX_BYTES, EFFECTS, FORMATS, coreSchema, contractSchema, implementationSchema, } from "./artifacts.js"; +export { InvalidDocument, MAX_DEPTH, decodeUtf8, parse, canonicalize, digest } from "./json.js"; +export { sha256 } from "./sha256.js"; +export { descriptionErrors, parseDescription } from "./description.js"; +export { CAPABILITY_KINDS, Contract, contractErrors, } from "./contract.js"; +export { implementationErrors, parseImplementation, } from "./implementation.js"; +export { unportablePattern } from "./patterns.js"; diff --git a/dist/core/json.d.ts b/dist/core/json.d.ts new file mode 100644 index 0000000..5f253b4 --- /dev/null +++ b/dist/core/json.d.ts @@ -0,0 +1,23 @@ +/** A document that is not MAP JSON, or not the document it claims to be. */ +export declare class InvalidDocument extends Error { + readonly errors: string[]; + constructor(message: string, errors?: string[]); +} +export declare const MAX_DEPTH = 32; +/** The bytes as text, refusing anything but well-formed UTF-8. */ +export declare function decodeUtf8(input: Uint8Array): string; +/** + * Parse MAP JSON from its original text: UTF-8 I-JSON (RFC 7493) of at most `maxBytes`, with + * no byte order mark, duplicate member names, lone surrogates, noncharacters or U+0000, no + * container deeper than 32 counting the root as 1, and every number a finite token of at most + * 64 characters within ±(2^53−1) that does not underflow to zero. These rules apply to the + * original tokens, before a general parser could erase them. + */ +export declare function parse(input: string | Uint8Array, maxBytes: number): unknown; +/** + * The RFC 8785 canonical form of a JSON value: members sorted by their UTF-16 code units, + * ECMAScript's minimal string escapes and its number format. + */ +export declare function canonicalize(value: unknown): string; +/** `sha-256:` and the SHA-256 of the value's RFC 8785 form, as MAP identifies contracts. */ +export declare const digest: (value: unknown) => string; diff --git a/dist/core/document.js b/dist/core/json.js similarity index 58% rename from dist/core/document.js rename to dist/core/json.js index e0965fc..68fc623 100644 --- a/dist/core/document.js +++ b/dist/core/json.js @@ -1,38 +1,46 @@ -// Parsing MAP documents as I-JSON within the core limits. -import { createHash } from 'node:crypto'; -/** A MAP document that is not I-JSON within the core limits. */ +// MAP JSON: I-JSON parsed from its original text, and the RFC 8785 digests that identify +// contracts and descriptions. +import { sha256 } from "./sha256.js"; +/** A document that is not MAP JSON, or not the document it claims to be. */ export class InvalidDocument extends Error { + errors; + constructor(message, errors = [message]) { + super(message); + this.errors = errors; + } } -export const MAX_BYTES = 64 * 1024; export const MAX_DEPTH = 32; +const MAX_NUMBER_TOKEN = 64; const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? `\\u{${(plane * 0x10000 + 0xfffe).toString(16)}}\\u{${(plane * 0x10000 + 0xffff).toString(16)}}`).join('')}]`, 'u'); +const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/; +const ZERO = /^-?0(?:\.0+)?(?:[eE][+-]?[0-9]+)?$/; +const forbidden = (point) => point === 0 || (point >= 0xfdd0 && point <= 0xfdef) || (point & 0xfffe) === 0xfffe; +/** The bytes as text, refusing anything but well-formed UTF-8. */ +export function decodeUtf8(input) { + try { + // The byte order mark is kept, so parsing refuses it. + return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(input); + } + catch { + throw new InvalidDocument('The JSON is not valid UTF-8.'); + } +} /** - * Parse a MAP document as I-JSON (RFC 7493) within the core limits: valid UTF-8 with no - * byte order mark, at most 64 KiB, no duplicate member names, no lone surrogates, - * noncharacters or U+0000 in any string, every number within ±(2^53−1), and arrays and - * objects nested at most 32 deep, the outermost counting as one. Text is measured in its - * UTF-8 bytes; bytes must be strict UTF-8. + * Parse MAP JSON from its original text: UTF-8 I-JSON (RFC 7493) of at most `maxBytes`, with + * no byte order mark, duplicate member names, lone surrogates, noncharacters or U+0000, no + * container deeper than 32 counting the root as 1, and every number a finite token of at most + * 64 characters within ±(2^53−1) that does not underflow to zero. These rules apply to the + * original tokens, before a general parser could erase them. */ -export function parse(input) { - let text; - if (typeof input === 'string') - text = input; - else { - try { - // The byte order mark is kept, so the parser refuses it. - text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(input); - } - catch { - throw new InvalidDocument('The MAP document is not valid UTF-8.'); - } - } - if (new TextEncoder().encode(text).length > MAX_BYTES) - throw new InvalidDocument(`The MAP document exceeds ${MAX_BYTES} bytes.`); +export function parse(input, maxBytes) { + const text = typeof input === 'string' ? input : decodeUtf8(input); + if (text.charCodeAt(0) === 0xfeff) + throw new InvalidDocument('A byte order mark is not allowed.'); + if (new TextEncoder().encode(text).length > maxBytes) + throw new InvalidDocument(`The JSON exceeds ${maxBytes} bytes.`); let index = 0; - const fail = (message) => { - throw new InvalidDocument(`Invalid MAP JSON at offset ${index}: ${message}`); + const fail = (reason) => { + throw new InvalidDocument(`Invalid MAP JSON at offset ${index}: ${reason}.`); }; const space = () => { while (' \t\n\r'.includes(text[index] ?? '-')) @@ -55,13 +63,12 @@ export function parse(input) { } if (LONE_SURROGATE.test(value)) fail('lone surrogate'); - if (value.includes('\u0000')) - fail('U+0000 in a string'); - if (NONCHARACTER.test(value)) - fail('a noncharacter in a string'); + for (const character of value) + if (forbidden(character.codePointAt(0))) + fail('U+0000 or a noncharacter'); return value; }; - // `depth` counts the arrays and objects enclosing the value. + // `depth` counts the containers enclosing the value. const value = (depth) => { space(); const next = text[index]; @@ -131,15 +138,23 @@ export function parse(input) { } if (next === '"') return string(); - const literal = /^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/.exec(text.slice(index)); - if (!literal) - fail('unexpected token'); - index += literal[0].length; - const parsed = JSON.parse(literal[0]); - if (typeof parsed === 'number' && - (!Number.isFinite(parsed) || Math.abs(parsed) > Number.MAX_SAFE_INTEGER)) - fail('number outside the I-JSON range'); - return parsed; + for (const literal of ['true', 'false', 'null']) + if (text.startsWith(literal, index)) { + index += literal.length; + return JSON.parse(literal); + } + const token = NUMBER.exec(text.slice(index))?.[0]; + if (token === undefined) + return fail('unexpected token'); + index += token.length; + if (token.length > MAX_NUMBER_TOKEN) + fail(`a number token longer than ${MAX_NUMBER_TOKEN} characters`); + const number = Number(token); + if (!Number.isFinite(number) || Math.abs(number) > Number.MAX_SAFE_INTEGER) + fail('a number outside the I-JSON range'); + if (number === 0 && !ZERO.test(token)) + fail('a number that underflows to zero'); + return number; }; const result = value(0); space(); @@ -158,8 +173,7 @@ const ESCAPES = { }; /** * The RFC 8785 canonical form of a JSON value: members sorted by their UTF-16 code units, - * ECMAScript's minimal string escapes and its number format. An undefined member is - * left out and an undefined array item is null, as JSON.stringify writes them. + * ECMAScript's minimal string escapes and its number format. */ export function canonicalize(value) { if (value === null || typeof value === 'boolean') @@ -172,19 +186,15 @@ export function canonicalize(value) { if (typeof value === 'string') return `"${value.replace(/["\\\u0000-\u001f]/g, (char) => ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)}"`; if (Array.isArray(value)) - return `[${value.map((item) => (item === undefined ? 'null' : canonicalize(item))).join(',')}]`; + return `[${value.map(canonicalize).join(',')}]`; if (typeof value === 'object') { const members = value; return `{${Object.keys(members) - .filter((name) => members[name] !== undefined) .sort() .map((name) => `${canonicalize(name)}:${canonicalize(members[name])}`) .join(',')}}`; } throw new TypeError(`${typeof value} is not a JSON value.`); } -/** - * `sha-256:` and the SHA-256 of the RFC 8785 canonical form: the digest MAP uses for - * descriptions, contracts and pinned schemas. - */ -export const digest = (value) => `sha-256:${createHash('sha256').update(canonicalize(value)).digest('hex')}`; +/** `sha-256:` and the SHA-256 of the value's RFC 8785 form, as MAP identifies contracts. */ +export const digest = (value) => `sha-256:${sha256(canonicalize(value))}`; diff --git a/dist/core/limits.d.ts b/dist/core/limits.d.ts deleted file mode 100644 index 3a77f28..0000000 --- a/dist/core/limits.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { InputError, JsonObject } from './types.ts'; -/** The text, or its first limit - 1 code points and an ellipsis. */ -export declare const cut: (text: string, limit: number) => string; -/** - * An input error within the core problem's limits. A pointer too long to report names its - * nearest ancestor that fits, with a detail that says so, and a detail too long is cut. - */ -export declare function inputError(detail: string, pointer: string): InputError; -/** - * A problem's title, detail and input errors within the core limits. An empty or overlong - * title, an empty detail or an empty error list is the caller's mistake. - */ -export declare function problemMembers(title: string, detail: string, errors?: InputError[]): { - title: string; - detail: string; - errors: InputError[] | undefined; -}; -/** The problem with as many of its errors, in order, as fit within the document limit. */ -export declare function withinDocument(problem: JsonObject): JsonObject; diff --git a/dist/core/limits.js b/dist/core/limits.js deleted file mode 100644 index 4973f2b..0000000 --- a/dist/core/limits.js +++ /dev/null @@ -1,55 +0,0 @@ -// MAP documents within the core limits, with lengths counted in code points as JSON -// Schema counts them. -import { MAX_BYTES } from "./document.js"; -import { codePoints, within } from "./pointer.js"; -const TITLE = 240; -const DETAIL = 4000; -const POINTER = 1000; -const ERROR_DETAIL = 2000; -const ERRORS = 100; -/** The text, or its first limit - 1 code points and an ellipsis. */ -export const cut = (text, limit) => codePoints(text) > limit ? `${[...text].slice(0, limit - 1).join('')}…` : text; -/** - * An input error within the core problem's limits. A pointer too long to report names its - * nearest ancestor that fits, with a detail that says so, and a detail too long is cut. - */ -export function inputError(detail, pointer) { - if (codePoints(pointer) > POINTER) - return { - detail: 'A member within this value does not satisfy the contract.', - pointer: within(pointer, POINTER), - }; - return { detail: cut(detail, ERROR_DETAIL), pointer }; -} -/** - * A problem's title, detail and input errors within the core limits. An empty or overlong - * title, an empty detail or an empty error list is the caller's mistake. - */ -export function problemMembers(title, detail, errors) { - if (codePoints(title) < 1 || codePoints(title) > TITLE) - throw new RangeError(`A problem title is 1 to ${TITLE} characters.`); - if (!detail) - throw new RangeError('A problem detail is not empty.'); - if (errors) { - if (!errors.length) - throw new RangeError('Errors, when given, name at least one input error.'); - if (errors.some((error) => !error.detail)) - throw new RangeError('An input error has a detail.'); - } - return { - title, - detail: cut(detail, DETAIL), - errors: errors?.slice(0, ERRORS).map((error) => inputError(error.detail, error.pointer)), - }; -} -/** The problem with as many of its errors, in order, as fit within the document limit. */ -export function withinDocument(problem) { - const errors = problem.errors; - if (!errors) - return problem; - const kept = [...errors]; - const size = () => new TextEncoder().encode(JSON.stringify({ ...problem, errors: kept })).length; - while (kept.length > 1 && size() > MAX_BYTES) - kept.pop(); - return { ...problem, errors: kept }; -} diff --git a/dist/core/patterns.d.ts b/dist/core/patterns.d.ts new file mode 100644 index 0000000..344c4da --- /dev/null +++ b/dist/core/patterns.d.ts @@ -0,0 +1,12 @@ +/** + * Why a pattern is outside the portable subset, or undefined. The subset is printable ASCII + * text, with any other code point written as \uXXXX. It has literal characters and escaped + * syntax characters; \t, \n, \f, \r and \d, the ASCII digits; non-empty classes of literal or + * escaped members and ranges, with a literal hyphen only first or last; (?: groups; + * alternation; ^ and $; and one *, +, ?, {n}, {n,} or {n,m} after an atom. Other class + * escapes and the dot match different characters in different engines, and brackets, && or a + * stray brace inside a class, or a quantifier on a quantifier, are read differently or refused + * by one of them. ^ and $ anchor the whole value, as in ECMA-262; an engine whose anchors also + * match at line breaks must read them so. + */ +export declare function unportablePattern(pattern: string): string | undefined; diff --git a/dist/core/patterns.js b/dist/core/patterns.js new file mode 100644 index 0000000..5f62514 --- /dev/null +++ b/dist/core/patterns.js @@ -0,0 +1,165 @@ +// The regular expressions a contract's schemas may use: the subset that ECMA-262 (with the +// u flag) and other engines read alike, so every implementation enforces the same contract. +const SYNTAX_CHARACTERS = '^$\\.*+?()[]{}|/'; +const CONTROL_ESCAPES = { t: 0x09, n: 0x0a, f: 0x0c, r: 0x0d }; +const QUANTIFIERS = '*+?{'; +/** + * Why a pattern is outside the portable subset, or undefined. The subset is printable ASCII + * text, with any other code point written as \uXXXX. It has literal characters and escaped + * syntax characters; \t, \n, \f, \r and \d, the ASCII digits; non-empty classes of literal or + * escaped members and ranges, with a literal hyphen only first or last; (?: groups; + * alternation; ^ and $; and one *, +, ?, {n}, {n,} or {n,m} after an atom. Other class + * escapes and the dot match different characters in different engines, and brackets, && or a + * stray brace inside a class, or a quantifier on a quantifier, are read differently or refused + * by one of them. ^ and $ anchor the whole value, as in ECMA-262; an engine whose anchors also + * match at line breaks must read them so. + */ +export function unportablePattern(pattern) { + if (!/^[\x20-\x7e]*$/.test(pattern)) + return 'a character outside printable ASCII'; + let at = 0; + // One escaped code point, the digit class outside a class, or a problem. + const escape = (inClass) => { + const next = pattern[at + 1]; + if (next === undefined) + return 'a trailing backslash'; + if (next === 'u') { + const digits = pattern.slice(at + 2, at + 6); + if (!/^[0-9a-fA-F]{4}$/.test(digits)) + return 'a malformed \\u escape'; + const code = parseInt(digits, 16); + if (code >= 0xd800 && code <= 0xdfff) + return 'a surrogate \\u escape'; + at += 6; + return code; + } + at += 2; + if (next === 'd' && !inClass) + return -1; + if (Object.hasOwn(CONTROL_ESCAPES, next)) + return CONTROL_ESCAPES[next]; + if (SYNTAX_CHARACTERS.includes(next) || (inClass && next === '-')) + return next.charCodeAt(0); + return `the escape \\${next}`; + }; + const characterClass = () => { + at += pattern[at + 1] === '^' ? 2 : 1; + // Each member's code point, or null for an unescaped hyphen. + const members = []; + while (pattern[at] !== ']') { + const char = pattern[at]; + if (char === undefined) + return 'an unclosed character class'; + if (char === '[') + return 'a bracket inside a class'; + if (char === '&' && pattern[at + 1] === '&') + return '&& inside a class'; + if (char === '\\') { + const code = escape(true); + if (typeof code === 'string') + return code; + members.push(code); + } + else { + members.push(char === '-' ? null : char.charCodeAt(0)); + at += 1; + } + } + at += 1; + if (!members.length) + return 'an empty character class'; + for (let index = 0; index < members.length; index += 1) { + const low = members[index]; + if (low === null) { + if (index !== 0 && index !== members.length - 1) + return 'an ambiguous hyphen in a class'; + continue; + } + if (members[index + 1] !== null || index + 2 >= members.length) + continue; + const high = members[index + 2]; + if (high === null) + return 'an ambiguous hyphen in a class'; + if (high < low) + return 'a class range out of order'; + index += 2; + if (members[index + 1] === null && index + 1 !== members.length - 1) + return 'an ambiguous hyphen in a class'; + } + }; + const quantifier = () => { + if (pattern[at] === '{') { + const bounds = /^\{([0-9]{1,4})(?:(,)([0-9]{1,4})?)?\}/.exec(pattern.slice(at)); + if (!bounds) + return 'a malformed {n,m} quantifier'; + if (bounds[3] !== undefined && Number(bounds[3]) < Number(bounds[1])) + return 'a {n,m} quantifier with m below n'; + at += bounds[0].length; + } + else + at += 1; + const next = pattern[at]; + if (next !== undefined && QUANTIFIERS.includes(next)) + return 'a quantifier on a quantifier'; + }; + const disjunction = (depth) => { + let quantifiable = false; + while (at < pattern.length) { + const char = pattern[at]; + if (char === '|') { + at += 1; + quantifiable = false; + } + else if (char === ')') { + if (depth === 0) + return 'an unmatched )'; + return; + } + else if (char === '^' || char === '$') { + at += 1; + quantifiable = false; + } + else if (QUANTIFIERS.includes(char)) { + if (!quantifiable) + return char === '{' ? 'an unescaped {' : 'a quantifier with nothing to repeat'; + const problem = quantifier(); + if (problem) + return problem; + quantifiable = false; + } + else if (char === '(') { + if (!pattern.startsWith('(?:', at)) + return 'a group other than (?:'; + at += 3; + const problem = disjunction(depth + 1); + if (problem) + return problem; + if (pattern[at] !== ')') + return 'an unclosed group'; + at += 1; + quantifiable = true; + } + else if (char === '[') { + const problem = characterClass(); + if (problem) + return problem; + quantifiable = true; + } + else if (char === '\\') { + const code = escape(false); + if (typeof code === 'string') + return code; + quantifiable = true; + } + else if (char === '.') + return 'an unescaped dot'; + else if (char === ']' || char === '}') + return `an unescaped ${char}`; + else { + at += 1; + quantifiable = true; + } + } + }; + return disjunction(0); +} diff --git a/dist/core/pointer.d.ts b/dist/core/pointer.d.ts deleted file mode 100644 index 25f3701..0000000 --- a/dist/core/pointer.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export declare const escape: (name: string) => string; -export declare const segments: (pointer: string) => string[]; -export declare const codePoints: (text: string) => number; -/** The pointer, or its nearest ancestor no longer than limit code points. */ -export declare function within(pointer: string, limit: number): string; -/** The value a pointer names through objects and arrays, or undefined. */ -export declare function resolve(value: unknown, pointer: string): unknown; -/** - * The value a pointer names through object members only, or undefined. The contract - * rules bind form fields only through object properties, so no binding indexes an array. - */ -export declare function member(value: unknown, pointer: string): unknown; diff --git a/dist/core/pointer.js b/dist/core/pointer.js deleted file mode 100644 index dc2f4c9..0000000 --- a/dist/core/pointer.js +++ /dev/null @@ -1,48 +0,0 @@ -// JSON Pointer (RFC 6901). Undefined stands for "nothing there", which no JSON value is. -const INDEX = /^(?:0|[1-9][0-9]*)$/; -export const escape = (name) => name.replaceAll('~', '~0').replaceAll('/', '~1'); -export const segments = (pointer) => pointer - .split('/') - .slice(1) - .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~')); -// Lengths as JSON Schema counts them: in code points, not UTF-16 code units. -export const codePoints = (text) => [...text].length; -/** The pointer, or its nearest ancestor no longer than limit code points. */ -export function within(pointer, limit) { - const tokens = pointer.split('/'); - while (codePoints(tokens.join('/')) > limit) - tokens.pop(); - return tokens.join('/'); -} -/** The value a pointer names through objects and arrays, or undefined. */ -export function resolve(value, pointer) { - let current = value; - for (const name of segments(pointer)) { - if (Array.isArray(current)) { - if (!INDEX.test(name) || Number(name) >= current.length) - return undefined; - current = current[Number(name)]; - } - else if (current && typeof current === 'object' && Object.hasOwn(current, name)) - current = current[name]; - else - return undefined; - } - return current; -} -/** - * The value a pointer names through object members only, or undefined. The contract - * rules bind form fields only through object properties, so no binding indexes an array. - */ -export function member(value, pointer) { - let current = value; - for (const name of segments(pointer)) { - if (!current || - typeof current !== 'object' || - Array.isArray(current) || - !Object.hasOwn(current, name)) - return undefined; - current = current[name]; - } - return current; -} diff --git a/dist/core/schemas.d.ts b/dist/core/schemas.d.ts deleted file mode 100644 index d426e3c..0000000 --- a/dist/core/schemas.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { JsonObject } from './types.ts'; -/** Every schema object within a schema, the schema itself first. */ -export declare function nodes(schema: unknown): Generator; -/** Every value a keyword has in the schema objects within a schema. */ -export declare const values: (schema: unknown, keyword: string) => unknown[]; -/** Rules of a form fields block that JSON Schema cannot state. */ -export declare function formProblems(fields: JsonObject): string[]; -/** - * The schema the values of a fields block satisfy: only its fields, each with a text - * field's `format` read as the core lexical form of that name. - */ -export declare function fieldValuesSchema(fields: JsonObject): JsonObject; -/** - * Why a contract's schemas would resolve anything but a pinned schema, or undefined. No - * keyword resolves by scope, in the contract's schemas or the pinned ones it supplies. In - * the contract's own schemas, no $id or $schema appears but at the request schema's root, - * since it would move references or change the dialect a subschema is read in; no type is - * a list; autocomplete, the form fields block's annotation, is absent; and every - * reference is a string naming a schema in the pinned documents, by URL. - */ -export declare function referenceProblem(inline: unknown[], requestSchema: JsonObject, pinned: ReadonlyMap, supplied: JsonObject[]): string | undefined; diff --git a/dist/core/schemas.js b/dist/core/schemas.js deleted file mode 100644 index b9469a0..0000000 --- a/dist/core/schemas.js +++ /dev/null @@ -1,125 +0,0 @@ -// The schema objects within a schema, the form fields block and the references a contract's -// schemas make. -import { CORE_SCHEMA } from "./artifacts.js"; -import { resolve } from "./pointer.js"; -// The keywords whose value is a schema, a list of schemas, or a map from names to schemas. -// A name in a map is never a keyword, whatever it is called. -const SUBSCHEMA = [ - 'additionalProperties', - 'propertyNames', - 'items', - 'contains', - 'not', - 'if', - 'then', - 'else', - 'unevaluatedItems', - 'unevaluatedProperties', - 'contentSchema', -]; -const LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']; -const MAPS = ['properties', 'patternProperties', '$defs', 'dependentSchemas']; -/** Every schema object within a schema, the schema itself first. */ -export function* nodes(schema) { - if (!schema || typeof schema !== 'object' || Array.isArray(schema)) - return; - const node = schema; - yield node; - for (const keyword of SUBSCHEMA) - yield* nodes(node[keyword]); - for (const keyword of LISTS) - if (Array.isArray(node[keyword])) - for (const item of node[keyword]) - yield* nodes(item); - for (const keyword of MAPS) { - const map = node[keyword]; - if (map && typeof map === 'object' && !Array.isArray(map)) - for (const item of Object.values(map)) - yield* nodes(item); - } -} -/** Every value a keyword has in the schema objects within a schema. */ -export const values = (schema, keyword) => [...nodes(schema)].filter((node) => Object.hasOwn(node, keyword)).map((node) => node[keyword]); -// The core definitions that give a text field's `format` its lexical form. -const FORMATS = { - email: 'address', - uri: 'identifier', - date: 'date', - 'date-time': 'dateTime', -}; -/** Rules of a form fields block that JSON Schema cannot state. */ -export function formProblems(fields) { - const properties = (fields.properties ?? {}); - const problems = (fields.required ?? []) - .filter((name) => !Object.hasOwn(properties, name)) - .map((name) => `required field ${name} is not defined`); - for (const [name, field] of Object.entries(properties)) { - const choices = (field.oneOf ?? field.items?.anyOf ?? []).map((choice) => choice.const); - if (new Set(choices).size !== choices.length) - problems.push(`field ${name} repeats a choice`); - const defaults = field.default === undefined ? [] : [field.default].flat(); - if (choices.length && defaults.some((value) => !choices.includes(value))) - problems.push(`field ${name} defaults to a value it does not offer`); - } - return problems; -} -/** - * The schema the values of a fields block satisfy: only its fields, each with a text - * field's `format` read as the core lexical form of that name. - */ -export function fieldValuesSchema(fields) { - const properties = Object.fromEntries(Object.entries((fields.properties ?? {})).map(([name, field]) => { - if (typeof field.format !== 'string') - return [name, field]; - const { format, ...rest } = field; - return [name, { ...rest, $ref: `${CORE_SCHEMA}#/$defs/${FORMATS[format]}` }]; - })); - return { ...fields, properties, additionalProperties: false }; -} -// A plain JSON Pointer fragment: nothing percent-encoded, nothing to decode. -const FRAGMENT = /^(?:\/[A-Za-z0-9._~!$&'()*+,;=:@-]*)*$/; -// Keywords that resolve by scope rather than by a pinned address. -const UNPINNED = ['$dynamicRef', '$recursiveRef', '$dynamicAnchor', '$recursiveAnchor', '$anchor']; -/** - * Why a contract's schemas would resolve anything but a pinned schema, or undefined. No - * keyword resolves by scope, in the contract's schemas or the pinned ones it supplies. In - * the contract's own schemas, no $id or $schema appears but at the request schema's root, - * since it would move references or change the dialect a subschema is read in; no type is - * a list; autocomplete, the form fields block's annotation, is absent; and every - * reference is a string naming a schema in the pinned documents, by URL. - */ -export function referenceProblem(inline, requestSchema, pinned, supplied) { - for (const schema of [...inline, requestSchema, ...supplied]) { - const found = UNPINNED.find((keyword) => values(schema, keyword).length); - if (found) - return `${found} is not pinned by any digest`; - } - for (const schema of [...inline, requestSchema]) { - const all = [...nodes(schema)]; - const nested = schema === requestSchema ? all.slice(1) : all; - if (nested.some((node) => '$id' in node)) - return 'a nested $id would move references'; - if (nested.some((node) => '$schema' in node)) - return 'a nested $schema would change the dialect'; - if (all.some((node) => Array.isArray(node.type))) - return 'type names one type, never a list'; - if (all.some((node) => 'autocomplete' in node)) - return 'autocomplete belongs to the form fields block'; - for (const ref of values(schema, '$ref')) { - if (typeof ref !== 'string') - return 'a $ref is not a string'; - const [base, fragment = ''] = ref.split(/#(.*)/s); - const document = pinned.get(base); - if (!document) - return `${ref} is not a pinned dependency`; - if (fragment && !fragment.startsWith('/')) - return `${ref} names an anchor, not a JSON Pointer`; - if (!FRAGMENT.test(fragment)) - return `${ref} is not a plain JSON Pointer`; - const target = resolve(document, fragment); - if (typeof target !== 'boolean' && - !(target && typeof target === 'object' && !Array.isArray(target))) - return `${ref} does not name a schema`; - } - } -} diff --git a/dist/core/sha256.d.ts b/dist/core/sha256.d.ts new file mode 100644 index 0000000..5f43fd3 --- /dev/null +++ b/dist/core/sha256.d.ts @@ -0,0 +1,2 @@ +/** The lowercase hexadecimal SHA-256 of the bytes, or of the text's UTF-8 encoding. */ +export declare function sha256(input: string | Uint8Array): string; diff --git a/dist/core/sha256.js b/dist/core/sha256.js new file mode 100644 index 0000000..b18b937 --- /dev/null +++ b/dist/core/sha256.js @@ -0,0 +1,59 @@ +// SHA-256 (FIPS 180-4) over bytes, so the core needs no platform crypto and runs the same +// in Node, browsers and workers. +const K = Uint32Array.from([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]); +const rotate = (value, bits) => (value >>> bits) | (value << (32 - bits)); +/** The lowercase hexadecimal SHA-256 of the bytes, or of the text's UTF-8 encoding. */ +export function sha256(input) { + const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input; + // The message, a 1 bit, zeros, and its length in bits as a 64-bit big-endian integer. + const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64); + padded.set(bytes); + padded[bytes.length] = 0x80; + const view = new DataView(padded.buffer); + view.setUint32(padded.length - 8, Math.floor(bytes.length / 0x20000000)); + view.setUint32(padded.length - 4, (bytes.length * 8) >>> 0); + const hash = Uint32Array.from([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]); + const w = new Uint32Array(64); + for (let block = 0; block < padded.length; block += 64) { + for (let t = 0; t < 16; t += 1) + w[t] = view.getUint32(block + t * 4); + for (let t = 16; t < 64; t += 1) { + const s0 = rotate(w[t - 15], 7) ^ rotate(w[t - 15], 18) ^ (w[t - 15] >>> 3); + const s1 = rotate(w[t - 2], 17) ^ rotate(w[t - 2], 19) ^ (w[t - 2] >>> 10); + w[t] = w[t - 16] + s0 + w[t - 7] + s1; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let t = 0; t < 64; t += 1) { + const t1 = h + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[t] + w[t]; + const t2 = (rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + h = g; + g = f; + f = e; + e = (d + t1) >>> 0; + d = c; + c = b; + b = a; + a = (t1 + t2) >>> 0; + } + hash[0] += a; + hash[1] += b; + hash[2] += c; + hash[3] += d; + hash[4] += e; + hash[5] += f; + hash[6] += g; + hash[7] += h; + } + return Array.from(hash, (word) => word.toString(16).padStart(8, '0')).join(''); +} diff --git a/dist/core/types.d.ts b/dist/core/types.d.ts deleted file mode 100644 index db06053..0000000 --- a/dist/core/types.d.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** The documents MAP 0.2 defines and the type contracts that give them meaning. */ -export type JsonObject = Record; -export type Authority = 'credential' | 'possession'; -export type ResultState = 'accepted' | 'completed' | 'failed' | 'pending' | 'approval-required'; -export type Consequence = 'refusal' | 'protection' | 'record' | 'disclosure' | 'commitment' | 'authorization' | 'assertion'; -export interface SchemaReference { - url: string; - canonicalDigest: string; -} -export interface ContractOperation { - id: string; - effect: string; - authority: Authority[]; - consequences: Consequence[]; - repeatable?: boolean; - fieldBindings?: { - input: string; - fields: string; - }[]; - results: { - state: ResultState; - reasons?: string[]; - outputSchema: JsonObject; - }[]; -} -export interface TypeContract { - kind: 'MapTypeContract'; - id: string; - version: string; - profile: string; - target: string; - dependencies?: SchemaReference[]; - detailsSchema?: JsonObject; - requestSchema: SchemaReference; - operations: ContractOperation[]; -} -/** The exact type a description and its requests name. */ -export type TypeReference = { - id: string; - version: string; - contractDigest: string; -}; -export type Target = { - id: string; - revision: string; - digest: string; - title?: string; -}; -export type MapDescription = { - '@context': string; - '@type': 'MailAction'; - '@id': string; - profile: string; - type: TypeReference; - describedAt: string; - expiresAt: string; - service: { - id: string; - name: string; - authority: Authority; - resource?: string; - execution: { - url: string; - resultUrlTemplate: string; - resultRetentionSeconds: number; - }; - onBehalfOf?: { - id: string; - name: string; - }; - humanUrl: string; - }; - recipient?: string; - target: Target; - details?: JsonObject; - operations: { - id: string; - name: string; - description: string; - }[]; -}; -export type MapRequest = { - kind: 'MapRequest'; - profile: string; - requestId: string; - interactionId: string; - descriptionDigest: string; - type: TypeReference; - operation: string; - input: JsonObject; -}; -export type MapResult = { - kind: 'MapResult'; - profile: string; - requestId: string; - interactionId: string; - descriptionDigest: string; - type: TypeReference; - operation: string; - state: ResultState; - target: Target; - recordedAt: string; - resultUrl: string; - actor?: string; - approvalUrl?: string; - reason?: string; - output: JsonObject; -}; -/** An RFC 9457 problem, with MAP's correlation members when it is correlated. */ -export type MapProblem = { - type: string; - title: string; - status: number; - detail: string; - instance?: string; - profile?: string; - requestId?: string; - interactionId?: string; - code?: string; - target?: Target; - errors?: InputError[]; -}; -/** One way a request's input fails its contract, with a JSON Pointer into the input. */ -export type InputError = { - detail: string; - pointer: string; -}; diff --git a/dist/core/types.js b/dist/core/types.js deleted file mode 100644 index 403e0de..0000000 --- a/dist/core/types.js +++ /dev/null @@ -1,2 +0,0 @@ -/** The documents MAP 0.2 defines and the type contracts that give them meaning. */ -export {}; diff --git a/dist/forms-0.1.schema.json b/dist/forms-0.1.schema.json deleted file mode 100644 index 88f680b..0000000 --- a/dist/forms-0.1.schema.json +++ /dev/null @@ -1,329 +0,0 @@ -{ - "$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/implementation.schema.json b/dist/implementation.schema.json new file mode 100644 index 0000000..f6d2310 --- /dev/null +++ b/dist/implementation.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/implementation.schema.json", + "title": "MAP implementation record", + "type": "object", + "additionalProperties": false, + "required": [ + "service", + "maintainer", + "type", + "operations", + "binding", + "status", + "documentation", + "evidence" + ], + "dependentRequired": { + "artifact": ["format"], + "format": ["artifact"] + }, + "properties": { + "service": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "maintainer": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + } + } + }, + "type": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "contractDigest" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "version": { + "type": "string", + "minLength": 1 + }, + "contractDigest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$" + } + } + }, + "operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "binding": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Identifier of the binding specification or documented service binding. It is not a request destination. Clients must explicitly support it." + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "url", + "digest", + "digestMode" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$" + }, + "digestMode": { + "enum": [ + "canonical-json", + "bytes" + ], + "description": "canonical-json hashes the RFC 8785 representation; bytes hashes the exact downloaded artifact. Both use SHA-256." + } + } + }, + "format": { + "type": "string", + "minLength": 1 + }, + "status": { + "enum": [ + "Draft", + "Experimental", + "Stable", + "Deprecated", + "Superseded" + ] + }, + "documentation": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "url", + "summary" + ], + "properties": { + "kind": { + "enum": [ + "declaration", + "test-report" + ] + }, + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + } + }, + "successor": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + } + } +} diff --git a/dist/index.d.ts b/dist/index.d.ts index cfaeee2..ae3dea4 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,10 +1,5 @@ +/** + * Mail Action Protocol 0.3 processing: parse and check descriptions, type contracts and + * implementation records, and compute the RFC 8785 digests that identify contracts. + */ export * from './core/index.js'; -export { contributionErrors, assertContribution, assertTypeRecord, referenceErrors, } from './validation.js'; -export type { CatalogView } from './validation.js'; -export { typeStages } from './model.js'; -export type { Party, TypeDefinition, TypeRecord, Contribution, Implementation } from './model.js'; -export { 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. */ -export declare function getRecordSchema(): Record; diff --git a/dist/index.js b/dist/index.js index df50567..ae3dea4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,17 +1,5 @@ /** - * Mail Action Protocol 0.2 processing and core artifacts, and validation tools for MailSchema - * Registry contributions. + * Mail Action Protocol 0.3 processing: parse and check descriptions, type contracts and + * implementation records, and compute the RFC 8785 digests that identify contracts. */ -import schema from './contribution.schema.json' with { type: 'json' }; export * from './core/index.js'; -export { contributionErrors, assertContribution, assertTypeRecord, referenceErrors, } from './validation.js'; -export { typeStages } from './model.js'; -export { 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); -} -/** Return the schema for an expanded Registry record, including attribution and history. */ -export function getRecordSchema() { - return { $schema: schema.$schema, $defs: structuredClone(schema.$defs), $ref: '#/$defs/record' }; -} diff --git a/dist/map-0.2.jsonld b/dist/map-0.2.jsonld deleted file mode 100644 index c97aca3..0000000 --- a/dist/map-0.2.jsonld +++ /dev/null @@ -1,91 +0,0 @@ -{ - "@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 deleted file mode 100644 index a87b566..0000000 --- a/dist/map-0.2.schema.json +++ /dev/null @@ -1,1033 +0,0 @@ -{ - "$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 deleted file mode 100644 index 6bf1b54..0000000 --- a/dist/map.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** A fresh copy of the MAP 0.2 core schema, as the profile record binds it by SHA-256. */ -export declare function getMapSchema(): 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 deleted file mode 100644 index 6b650b8..0000000 --- a/dist/map.js +++ /dev/null @@ -1,18 +0,0 @@ -import { readFileSync } from 'node:fs'; -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 artifact('map-0.2.schema.json'); -} -/** A fresh copy of the MAP 0.2 JSON-LD context. */ -export function getMapContext() { - return artifact('map-0.2.jsonld'); -} -/** 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'); -} -/** A fresh copy of the form fields block contracts pin. */ -export function getFormsSchema() { - return artifact('forms-0.1.schema.json'); -} diff --git a/dist/model.d.ts b/dist/model.d.ts deleted file mode 100644 index e459976..0000000 --- a/dist/model.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -export declare const typeStages: readonly [{ - readonly status: "Draft"; - readonly singular: "draft"; - readonly plural: "drafts"; -}, { - readonly status: "Proposal"; - readonly singular: "proposal"; - readonly plural: "proposals"; -}, { - readonly status: "Reuse assessment"; - readonly singular: "reuse assessment"; - readonly plural: "reuse assessments"; -}]; -export interface Party { - name: string; - url?: string; -} -export interface TypeDefinition { - slug: string; - name: string; - summary: string; - category: string; - status: (typeof typeStages)[number]['status']; - version: string | null; - profile: string; - overview: string; - target: string; - operations: { - id: string; - name: string; - description: string; - }[]; - inputs: string; - results: string; - permissions: string; - humanRoute: string; - example: { - title: string; - steps: string[]; - exception: string; - }; - references: { - title: string; - href: string; - description: string; - }[]; - openQuestions: string[]; - maintainers: Party[]; - definition?: { - label: string; - href: string; - }; -} -export interface TypeRecord extends TypeDefinition { - origin: string; - contributors: Party[]; - history: { - label: string; - description: string; - contributionId?: string; - }[]; -} -interface Submission { - format: 'mailschema-contribution/1'; - id: string; - contributor: Party; - source?: string; - summary: string; -} -export type Contribution = Submission & ({ - kind: 'new-type'; - record: TypeDefinition; -} | { - kind: 'amendment'; - baseDigest: string; - record: TypeDefinition; -} | { - kind: 'implementation'; - type: string; - typeVersion: string; - typeDigest: string; - profile: string; - product: Party; - operations: string[]; - evidence: { - kind: 'declaration' | 'test-report'; - url?: string; - reproduction?: string; - results?: string; - }; -}); -export type Implementation = Extract; -export {}; diff --git a/dist/model.js b/dist/model.js deleted file mode 100644 index 41ccef3..0000000 --- a/dist/model.js +++ /dev/null @@ -1,5 +0,0 @@ -export const typeStages = [ - { status: 'Draft', singular: 'draft', plural: 'drafts' }, - { status: 'Proposal', singular: 'proposal', plural: 'proposals' }, - { status: 'Reuse assessment', singular: 'reuse assessment', plural: 'reuse assessments' }, -]; diff --git a/dist/profile.json b/dist/profile.json new file mode 100644 index 0000000..ff28124 --- /dev/null +++ b/dist/profile.json @@ -0,0 +1,27 @@ +{ + "id": "https://mailschema.org/profiles/map/0.3", + "name": "Mail Action Protocol 0.3", + "status": "working-draft", + "description": "An action description carried in Structured Email. A trusted service binding maps each decision to the service's existing operations, and the service enforces permission and exact terms. Types are defined by digest-bound contracts.", + "context": "https://mailschema.org/contexts/map-0.3.jsonld", + "schema": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "contractFormat": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "specification": "https://mailschema.org/specification/core", + "artifacts": { + "context": { + "url": "https://mailschema.org/contexts/map-0.3.jsonld", + "sha256": "a51490d1fe8af695c14ea508aad88fd0429d18564791b6b52f3ab07d5c51aab3" + }, + "schema": { + "url": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "sha256": "0ad09a6955a47e7a2c1c1eec0747a3e0b15c08a79860e90ce4e65da11983aeb3" + }, + "contractFormat": { + "url": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "sha256": "86bf057529c4fd2c7f0a880825774afeed31e9f7ba1d99760858a585fe28d18d" + }, + "conformance": { + "url": "https://github.com/mailschema/mailschema/blob/main/conformance/map-0.3/README.md" + } + } +} diff --git a/dist/type-contract-0.2.schema.json b/dist/type-contract-0.2.schema.json deleted file mode 100644 index 1fe14b2..0000000 --- a/dist/type-contract-0.2.schema.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "$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/dist/validation.d.ts b/dist/validation.d.ts deleted file mode 100644 index 46df85e..0000000 --- a/dist/validation.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Contribution, TypeRecord } from './model.ts'; -export declare function contributionErrors(value: unknown): string[]; -export declare function assertContribution(value: unknown): asserts value is Contribution; -export declare function assertTypeRecord(value: unknown): asserts value is TypeRecord; -export interface CatalogView { - types: { - record: TypeRecord; - digest: string; - }[]; - snapshots: { - record: TypeRecord; - digest: string; - }[]; -} -export declare function referenceErrors(input: Contribution, catalog: CatalogView): string[]; diff --git a/dist/validation.js b/dist/validation.js deleted file mode 100644 index 5c8da40..0000000 --- a/dist/validation.js +++ /dev/null @@ -1,64 +0,0 @@ -import Ajv2020 from 'ajv/dist/2020.js'; -import addFormats from 'ajv-formats'; -import schema from './contribution.schema.json' with { type: 'json' }; -const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false }); -addFormats(ajv); -const contribution = ajv.compile(schema); -const variants = new Map(schema.oneOf.map((variant) => [ - variant.properties.kind.const, - ajv.compile({ $schema: schema.$schema, $defs: schema.$defs, ...variant }), -])); -const record = ajv.compile({ - $schema: schema.$schema, - $defs: schema.$defs, - $ref: '#/$defs/record', -}); -export function contributionErrors(value) { - const kind = value && typeof value === 'object' && 'kind' in value ? value.kind : undefined; - const validate = typeof kind === 'string' ? variants.get(kind) || contribution : contribution; - if (validate(value)) - return []; - return (validate.errors || []).map((error) => `${error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`); -} -export function assertContribution(value) { - const errors = contributionErrors(value); - if (errors.length) - throw new Error(`Invalid contribution:\n${errors.join('\n')}`); -} -export function assertTypeRecord(value) { - if (!record(value)) - throw new Error(`Invalid type record: ${ajv.errorsText(record.errors, { separator: '\n' })}`); -} -export function referenceErrors(input, catalog) { - const errors = []; - if (input.kind === 'implementation') { - const target = catalog.snapshots.find((item) => item.digest === input.typeDigest)?.record; - if (!target || target.slug !== input.type || target.version !== input.typeVersion) - return [ - 'Unknown type version or record digest. Download the current record from the Registry.', - ]; - if (target.profile !== input.profile) - errors.push('Execution profile does not match the targeted record.'); - const operations = new Set(target.operations.map((operation) => operation.id)); - if (new Set(input.operations).size !== input.operations.length || - input.operations.some((operation) => !operations.has(operation))) - errors.push('Unknown or duplicate operation in the implementation declaration.'); - } - else { - const existing = catalog.types.find((item) => item.record.slug === input.record.slug); - if (input.kind === 'new-type' && existing) - errors.push('This type already exists. Submit an amendment instead.'); - if (input.kind === 'amendment' && (!existing || existing.digest !== input.baseDigest)) - errors.push('Stale or unknown amendment base. Download the current record and reconcile your changes.'); - if (catalog.types.some((item) => item.record.slug !== input.record.slug && - item.record.name.toLocaleLowerCase() === input.record.name.toLocaleLowerCase())) - errors.push('Another type already uses this name.'); - const operationIds = input.record.operations.map((operation) => operation.id); - const operationNames = input.record.operations.map((operation) => operation.name.toLocaleLowerCase()); - if (new Set(operationIds).size !== operationIds.length) - errors.push('Operation identifiers must be unique within a type.'); - if (new Set(operationNames).size !== operationNames.length) - errors.push('Operation names must be unique within a type.'); - } - return errors; -} diff --git a/package-lock.json b/package-lock.json index 25e756c..0742c1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mailschema", - "version": "0.2.2", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mailschema", - "version": "0.2.2", + "version": "0.3.0", "license": "MIT", "dependencies": { "ajv": "8.20.0", diff --git a/package.json b/package.json index b714f83..44d554b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mailschema", - "version": "0.2.2", - "description": "Mail Action Protocol 0.2 processing and core artifacts, and MailSchema Registry validation", + "version": "0.3.0", + "description": "Mail Action Protocol 0.3: parse and check descriptions, type contracts and implementation records", "type": "module", "license": "MIT", "author": "MailSchema contributors", @@ -21,11 +21,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./contribution.schema.json": "./dist/contribution.schema.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", + "./profile.json": "./dist/profile.json", + "./context.jsonld": "./dist/context.jsonld", + "./core.schema.json": "./dist/core.schema.json", + "./contract.schema.json": "./dist/contract.schema.json", + "./implementation.schema.json": "./dist/implementation.schema.json", "./package.json": "./package.json" }, "types": "./dist/index.d.ts", @@ -52,10 +52,9 @@ }, "keywords": [ "email", - "schema", + "mail-action-protocol", "json-schema", - "agents", - "registry" + "agents" ], "publishConfig": { "access": "public", diff --git a/src/context.jsonld b/src/context.jsonld new file mode 100644 index 0000000..28c9635 --- /dev/null +++ b/src/context.jsonld @@ -0,0 +1,50 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "@vocab": "https://mailschema.org/vocab/map/0.3#", + "MailAction": "https://mailschema.org/vocab/map/0.3#MailAction", + "profile": { + "@id": "profile", + "@type": "@id" + }, + "type": { + "@id": "type", + "@type": "@json" + }, + "service": { + "@id": "service", + "@type": "@json" + }, + "subject": { + "@id": "subject", + "@type": "@json" + }, + "terms": { + "@id": "terms", + "@type": "@json" + }, + "details": { + "@id": "details", + "@type": "@json" + }, + "operations": { + "@id": "operations", + "@type": "@json" + }, + "human": { + "@id": "human", + "@type": "@json" + }, + "recipient": "recipient", + "inLanguage": "inLanguage", + "issuedAt": { + "@id": "issuedAt", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expiresAt": { + "@id": "expiresAt", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + } +} diff --git a/src/contract.schema.json b/src/contract.schema.json new file mode 100644 index 0000000..b050127 --- /dev/null +++ b/src/contract.schema.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "profile", + "name", + "summary", + "requirements", + "detailsSchema", + "operations" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.3" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "requirements": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "minItems": 1, + "maxItems": 64 + }, + "detailsSchema": { + "type": "object" + }, + "operations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "semantics", + "effects", + "bindings", + "actors", + "exactTerms", + "inputSchema", + "outcomes" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "semantics": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "effects": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "bindings": { + "type": "array", + "items": { + "enum": [ + "credential", + "capability" + ] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "actors": { + "type": "array", + "items": { + "enum": [ + "human", + "agent" + ] + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "exactTerms": { + "type": "boolean" + }, + "inputSchema": { + "type": [ + "object", + "null" + ] + }, + "outcomes": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "capability": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "maxLifetimeSeconds" + ], + "properties": { + "kind": { + "enum": [ + "refusal", + "protective-report", + "address-confirmation" + ] + }, + "maxLifetimeSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 604800 + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "bindings": { + "contains": { + "const": "capability" + }, + "type": "array" + } + } + }, + "then": { + "required": [ + "capability" + ], + "properties": { + "inputSchema": { + "type": "null" + }, + "exactTerms": { + "const": true + } + } + }, + "else": { + "not": { + "required": [ + "capability" + ] + } + } + } + ] + }, + "minItems": 1, + "maxItems": 32 + } + } +} diff --git a/src/contribution.schema.json b/src/contribution.schema.json deleted file mode 100644 index 6b7742a..0000000 --- a/src/contribution.schema.json +++ /dev/null @@ -1,874 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "MailSchema contribution format", - "description": "Repository contribution data. This is not a MAP message or wire representation.", - "$defs": { - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "slug": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "pattern": "\\S" - }, - "category": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "status": { - "enum": ["Draft", "Proposal", "Reuse assessment"] - }, - "version": { - "anyOf": [ - { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - { - "type": "null" - } - ] - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "overview": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "target": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "operations": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["id", "name", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "inputs": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "permissions": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "humanRoute": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "example": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "steps": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 1, - "maxItems": 100 - }, - "exception": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "steps", "exception"] - }, - "references": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "href", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "openQuestions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 0, - "maxItems": 100 - }, - "maintainers": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "pattern": "^/specification/(?:[a-z0-9-]+/)*$", - "maxLength": 300 - } - }, - "required": ["label", "href"] - } - }, - "required": [ - "slug", - "name", - "summary", - "category", - "status", - "version", - "profile", - "overview", - "target", - "operations", - "inputs", - "results", - "permissions", - "humanRoute", - "example", - "references", - "openQuestions", - "maintainers" - ], - "allOf": [ - { - "if": { - "properties": { - "status": { - "const": "Draft" - } - }, - "required": ["status"] - }, - "then": { - "required": ["definition"], - "properties": { - "version": { - "type": "string" - } - } - } - } - ] - }, - "record": { - "type": "object", - "additionalProperties": false, - "properties": { - "slug": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "pattern": "\\S" - }, - "category": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "status": { - "enum": ["Draft", "Proposal", "Reuse assessment"] - }, - "version": { - "anyOf": [ - { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - { - "type": "null" - } - ] - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "overview": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "target": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "operations": { - "type": "array", - "items": { - "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, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["id", "name", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "inputs": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "permissions": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "humanRoute": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "example": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "steps": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 1, - "maxItems": 100 - }, - "exception": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "steps", "exception"] - }, - "references": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["title", "href", "description"] - }, - "minItems": 1, - "maxItems": 100 - }, - "openQuestions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "minItems": 0, - "maxItems": 100 - }, - "maintainers": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "definition": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "href": { - "type": "string", - "pattern": "^/specification/(?:[a-z0-9-]+/)*$", - "maxLength": 300 - } - }, - "required": ["label", "href"] - }, - "origin": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "contributors": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "minItems": 1, - "maxItems": 100 - }, - "history": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "contributionId": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - } - }, - "required": ["label", "description"] - }, - "minItems": 1, - "maxItems": 100 - } - }, - "required": [ - "slug", - "name", - "summary", - "category", - "status", - "version", - "profile", - "overview", - "target", - "operations", - "inputs", - "results", - "permissions", - "humanRoute", - "example", - "references", - "openQuestions", - "maintainers", - "origin", - "contributors", - "history" - ], - "allOf": [ - { - "if": { - "properties": { - "status": { - "const": "Draft" - } - }, - "required": ["status"] - }, - "then": { - "required": ["definition"], - "properties": { - "version": { - "type": "string" - } - } - } - } - ] - } - }, - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "new-type" - }, - "record": { - "$ref": "#/$defs/definition" - } - }, - "required": ["format", "id", "contributor", "summary", "kind", "record"] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "amendment" - }, - "baseDigest": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "record": { - "$ref": "#/$defs/definition" - } - }, - "required": ["format", "id", "contributor", "summary", "kind", "baseDigest", "record"] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "format": { - "const": "mailschema-contribution/1" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "contributor": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "source": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "summary": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "kind": { - "const": "implementation" - }, - "type": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "typeVersion": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:-[a-zA-Z0-9.-]+)?$", - "maxLength": 80 - }, - "typeDigest": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "profile": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "product": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 240, - "pattern": "\\S" - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - } - }, - "required": ["name"] - }, - "operations": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", - "maxLength": 100 - }, - "minItems": 1, - "maxItems": 100 - }, - "evidence": { - "type": "object", - "additionalProperties": false, - "properties": { - "kind": { - "enum": ["declaration", "test-report"] - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "reproduction": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - }, - "results": { - "type": "string", - "minLength": 1, - "maxLength": 12000, - "pattern": "\\S" - } - }, - "required": ["kind"], - "allOf": [ - { - "if": { - "properties": { - "kind": { - "const": "test-report" - } - }, - "required": ["kind"] - }, - "then": { - "required": ["url", "reproduction", "results"] - } - } - ] - } - }, - "required": [ - "format", - "id", - "contributor", - "summary", - "kind", - "type", - "typeVersion", - "typeDigest", - "profile", - "product", - "operations", - "evidence" - ] - } - ] -} diff --git a/src/core.schema.json b/src/core.schema.json new file mode 100644 index 0000000..b0ebbfc --- /dev/null +++ b/src/core.schema.json @@ -0,0 +1,210 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "title": "MAP 0.3 description shape; semantic checks in Core are additionally required", + "type": "object", + "additionalProperties": false, + "required": [ + "@context", + "@type", + "@id", + "profile", + "type", + "issuedAt", + "expiresAt", + "inLanguage", + "service", + "recipient", + "subject", + "terms", + "details", + "operations", + "human" + ], + "properties": { + "@context": { + "const": "https://mailschema.org/contexts/map-0.3.jsonld" + }, + "@type": { + "const": "MailAction" + }, + "@id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "profile": { + "const": "https://mailschema.org/profiles/map/0.3" + }, + "type": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "contractDigest" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "contractDigest": { + "type": "string", + "minLength": 1, + "pattern": "^sha-256:[0-9a-f]{64}$" + } + } + }, + "issuedAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$" + }, + "expiresAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\dZ$" + }, + "inLanguage": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "service": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://(?![0-9.]+(:[0-9]+)?$)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(:[0-9]{1,5})?$", + "maxLength": 2048 + }, + "tenant": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + } + } + }, + "recipient": { + "type": "string", + "minLength": 1, + "maxLength": 320 + }, + "subject": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "terms": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "pattern": "^[ -~]+$", + "maxLength": 1024 + } + } + }, + "details": { + "type": "object" + }, + "operations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "capability": { + "type": "object", + "additionalProperties": false, + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://[^/?#@]+([/?][^#]*)?$", + "maxLength": 4096 + } + } + } + } + }, + "minItems": 1, + "maxItems": 32 + }, + "human": { + "type": "object", + "additionalProperties": false, + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://[^/?#@]+([/?][^#]*)?$", + "maxLength": 4096 + } + } + } + } +} diff --git a/src/core/artifacts.ts b/src/core/artifacts.ts index 4554992..c652a56 100644 --- a/src/core/artifacts.ts +++ b/src/core/artifacts.ts @@ -1,102 +1,34 @@ -// The bundled core artifacts and the validators built from them. -import Ajv2020, { type ErrorObject, type ValidateFunction } from 'ajv/dist/2020.js'; -import { contractFormat, coreSchema, formsSchema } from './bundled.ts'; -import type { JsonObject } from './types.ts'; - -export const MAP_PROFILE = 'https://mailschema.org/profiles/map/0.2'; -export const CORE_SCHEMA = 'https://mailschema.org/schemas/map-0.2.schema.json'; -export const FORMS_SCHEMA = 'https://mailschema.org/schemas/forms-0.1.schema.json'; -export const CONTRACT_FORMAT = 'https://mailschema.org/schemas/type-contract-0.2.schema.json'; -export const CONTEXT = 'https://mailschema.org/contexts/map-0.2.jsonld'; -export const JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema'; - -/** The schemas a contract may depend on without supplying them itself. */ -export const BUNDLED: ReadonlyMap = new Map([ - [CORE_SCHEMA, coreSchema as JsonObject], - [FORMS_SCHEMA, formsSchema as JsonObject], -]); +// MAP 0.3's identifiers, limits and schema validation. +import Ajv2020, { type ErrorObject } from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; + +export { coreSchema, contractSchema, implementationSchema } from './bundled.ts'; + +export const PROFILE = 'https://mailschema.org/profiles/map/0.3'; +export const CONTEXT = 'https://mailschema.org/contexts/map-0.3.jsonld'; +/** The Content-Type of the MIME part that carries a description. */ +export const DESCRIPTION_MEDIA_TYPE = `application/ld+json; profile="${PROFILE}"`; +export const DESCRIPTION_MAX_BYTES = 65536; +export const CONTRACT_MAX_BYTES = 262144; +/** The namespace of the initial effect vocabulary. */ +export const EFFECTS = 'https://mailschema.org/effects/'; +/** The formats MAP schemas may assert. Every implementation checks exactly these. */ +export const FORMATS = ['date', 'date-time', 'email', 'uri'] as const; /** - * JSON Schema 2020-12 as MAP reads it. Lexical forms are the core schema's patterns, so - * `format` stays an annotation: format checkers differ between validators. References - * resolve only against schemas already added, never over the network. + * JSON Schema 2020-12 as MAP reads it: the listed formats asserted, and references resolved + * only within the schema, never over the network. MAP's own rules decide which keywords and + * formats a contract may use; the validator adds no checks of its own beyond the vocabulary. * @internal */ export function createValidator() { - const ajv = new Ajv2020({ - allErrors: true, - strict: true, - strictRequired: false, - validateFormats: false, - }); - // HTML autofill field names label form fields; they are annotations, not assertions. - ajv.addKeyword({ keyword: 'autocomplete', schemaType: 'string' }); - // Only JSON Schema 2020-12 vocabulary keywords. Ajv also reads OpenAPI's `nullable`, - // which admits a null other validators refuse, and the earlier `dependencies` and - // `definitions`; strict mode now refuses them as unknown. + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv, [...FORMATS]); + // Ajv also reads OpenAPI's `nullable` and the earlier `dependencies` and `definitions`. for (const keyword of ['nullable', 'dependencies', 'definitions']) ajv.removeKeyword(keyword); - for (const schema of BUNDLED.values()) ajv.addSchema(schema); return ajv; } -const core = createValidator(); - -/** A core or form fields definition by name, for the shared lexical vectors. */ -export const definition = (schema: string, name: string): ValidateFunction => - core.getSchema(`${schema}#/$defs/${name}`)!; - -/** Errors as `pointer message` lines, as the other MailSchema packages report them. */ -export function errorList(errors: ErrorObject[] | null | undefined, prefix = '') { - return (errors ?? []) - .slice(0, 100) - .map( - (error) => - `${prefix + error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`, - ); -} - -const errorsOf = (validate: ValidateFunction, value: unknown) => - validate(value) ? [] : errorList(validate.errors); - -const DOCUMENT = core.getSchema(CORE_SCHEMA)!; -const DEFINITIONS = Object.fromEntries( - ['description', 'request', 'result', 'problem'].map((name) => [ - name, - definition(CORE_SCHEMA, name), - ]), -); -const DATE_TIME = new RegExp((coreSchema.$defs.dateTime as { pattern: string }).pattern, 'u'); -const REQUEST_ID = definition(CORE_SCHEMA, 'uuidUrn'); -const CONTRACT = core.compile(contractFormat); - -/** Checks a type contract against the contract format. */ -export const contractFormatErrors = (contract: unknown) => errorsOf(CONTRACT, contract); - -/** Checks any MAP 0.2 description, request, result or problem against the core schema. */ -export const mapErrors = (document: unknown) => errorsOf(DOCUMENT, document); - -/** - * Checks a description against the core description definition only. A description of a - * type the caller has no contract for can be checked this far. - */ -export const descriptionErrors = (description: unknown) => - errorsOf(DEFINITIONS.description, description); - -/** Checks a request against the core request definition. Its identifier stays free if it fails. */ -export const requestErrors = (request: unknown) => errorsOf(DEFINITIONS.request, request); - -/** Checks a result against the core result definition only. */ -export const resultErrors = (result: unknown) => errorsOf(DEFINITIONS.result, result); - -/** Checks a problem against the core problem definition, including its type, status and code. */ -export const problemErrors = (problem: unknown) => errorsOf(DEFINITIONS.problem, problem); - -/** Whether a value is a request identifier: the core UUID URN form. Only one names a result resource. */ -export const isRequestId = (value: unknown) => REQUEST_ID(value); - -/** - * Whether a deadline, a core date-time plus `afterSeconds`, has been reached. Anything but - * a core date-time counts as reached, so a mistake fails closed. - */ -export const reached = (now: Date, deadline: string, afterSeconds = 0) => - !(DATE_TIME.test(deadline) && now.getTime() < Date.parse(deadline) + afterSeconds * 1000); +/** Each validation error as its JSON Pointer and message. @internal */ +export const schemaErrors = (errors: ErrorObject[] | null | undefined, prefix = '') => + (errors ?? []).map((error) => `${prefix + error.instancePath || '/'}: ${error.message}`); diff --git a/src/core/binding.ts b/src/core/binding.ts deleted file mode 100644 index 9e8ea3c..0000000 --- a/src/core/binding.ts +++ /dev/null @@ -1,69 +0,0 @@ -// The HTTP and Structured Email bindings: capability URLs, the requests the execution URL -// admits, where results live and which message part carries a description. -import { MAP_PROFILE } from './artifacts.ts'; -import type { MapDescription } from './types.ts'; - -/** The Content-Type of the part that carries a description, labelled with the profile. */ -export const DESCRIPTION_MEDIA_TYPE = `application/ld+json; profile="${MAP_PROFILE}"`; - -/** - * Whether a designated part carries a description of this profile: its media type is - * application/ld+json, compared case-insensitively, and its profile parameter lists the - * profile among its space-separated URIs. - */ -export const isDescriptionPart = (mediaType: string, profileParameter: string | undefined) => - mediaType.toLowerCase() === 'application/ld+json' && - (profileParameter ?? '').split(/[ \t]+/).includes(MAP_PROFILE); - -/** The path of an absolute URL exactly as written, with no dot segments removed and nothing decoded. */ -export const writtenPath = (url: string) => - /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*([^?#]*)/.exec(url)?.[1] ?? ''; - -/** The possession capability: the last segment of the execution URL path, as written. */ -export const capability = (description: MapDescription) => - writtenPath(description.service.execution.url).split('/').at(-1) ?? ''; - -const UNGUESSABLE = /^[A-Za-z0-9_-]{22,}$/; -const dotSegment = (url: string) => - writtenPath(url) - .split('/') - .some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment)); - -/** The rules a possession description's capability URLs meet when it is issued. */ -export function capabilityProblems(description: MapDescription) { - const { url, resultUrlTemplate } = description.service.execution; - const issued = capability(description); - const problems: string[] = []; - if (!UNGUESSABLE.test(issued)) problems.push('The capability is too short to be unguessable.'); - if ([url, resultUrlTemplate].some(dotSegment)) - problems.push('A capability URL must not contain dot segments.'); - if (!resultUrlTemplate.startsWith(`${url}/`)) - problems.push('The result template must extend the execution URL and its capability.'); - if (description.service.humanUrl.includes(issued)) - problems.push('The human route must not carry the capability.'); - return problems; -} - -/** - * Whether a Content-Type admits a request rather than a 415: the media type - * application/json, compared case-insensitively, with no parameter other than a UTF-8 - * charset. Only HTTP's optional whitespace, spaces and tabs, may surround each part, and - * empty parameter slots are ignored, as RFC 9110 permits. - */ -export function isJsonRequest(contentType: string | undefined) { - const [type, ...parameters] = (contentType ?? '') - .split(';') - .map((part) => part.replace(/^[ \t]+|[ \t]+$/g, '').toLowerCase()); - return ( - type === 'application/json' && - parameters.every( - (parameter) => parameter === '' || /^charset=(?:utf-8|"utf-8")$/.test(parameter), - ) - ); -} - -/** The result resource of a request: the description's template with the identifier encoded. */ -export const resultUrl = (description: MapDescription, requestId: string) => - description.service.execution.resultUrlTemplate.replace('{requestId}', () => - encodeURIComponent(requestId), - ); diff --git a/src/core/bundled.ts b/src/core/bundled.ts index 8b865b1..8e6c623 100644 --- a/src/core/bundled.ts +++ b/src/core/bundled.ts @@ -1,8 +1,8 @@ -// The core artifacts, by their canonical files. Package preparation points these three -// imports at the package's own copies; no other line differs between the repository and -// the npm package. -import coreSchema from '../map-0.2.schema.json' with { type: 'json' }; -import formsSchema from '../forms-0.1.schema.json' with { type: 'json' }; -import contractFormat from '../type-contract-0.2.schema.json' with { type: 'json' }; +// The schemas the 0.3 profile record binds, by their canonical files. Package preparation +// points these three imports at the package's own copies; no other line differs between the +// repository and the npm package. +import coreSchema from '../core.schema.json' with { type: 'json' }; +import contractSchema from '../contract.schema.json' with { type: 'json' }; +import implementationSchema from '../implementation.schema.json' with { type: 'json' }; -export { coreSchema, formsSchema, contractFormat }; +export { coreSchema, contractSchema, implementationSchema }; diff --git a/src/core/contract.ts b/src/core/contract.ts index 9089c38..6d627cc 100644 --- a/src/core/contract.ts +++ b/src/core/contract.ts @@ -1,387 +1,288 @@ -// Type contracts an implementation vendors, verified by digest and compiled once. -import type { ErrorObject, ValidateFunction } from 'ajv/dist/2020.js'; +// A type contract: the digest-bound definition of an interaction's details, operations, +// effects and permitted bindings. +import type { ValidateFunction } from 'ajv/dist/2020.js'; import { - BUNDLED, - CORE_SCHEMA, - JSON_SCHEMA_2020_12, - MAP_PROFILE, - contractFormatErrors, + CONTRACT_MAX_BYTES, + contractSchema, createValidator, - descriptionErrors, - errorList, - reached, - requestErrors, - resultErrors, + EFFECTS, + FORMATS, + schemaErrors, } from './artifacts.ts'; -import { capabilityProblems } from './binding.ts'; -import { digest } from './document.ts'; -import type { ProblemCode } from './documents.ts'; -import { inputError } from './limits.ts'; -import { escape, member } from './pointer.ts'; -import { fieldValuesSchema, formProblems, referenceProblem, values } from './schemas.ts'; -import type { - InputError, - JsonObject, - MapDescription, - MapRequest, - MapResult, - TypeContract, - TypeReference, -} from './types.ts'; +import { descriptionErrors, type Description } from './description.ts'; +import { digest, InvalidDocument, parse } from './json.ts'; +import { unportablePattern } from './patterns.ts'; -/** A type contract that fails its own checks or its pinned digest. */ -export class InvalidContract extends Error {} +export interface Operation { + id: string; + name: string; + semantics: string; + effects: string[]; + bindings: ('credential' | 'capability')[]; + actors: ('human' | 'agent')[]; + exactTerms: boolean; + inputSchema: Record | null; + outcomes: Record; + capability?: { kind: CapabilityKind; maxLifetimeSeconds: number }; +} -/** Reasons the core assigns to the approval lifecycle. */ -export const APPROVAL_REASONS = ['declined', 'stale-target', 'expired', 'superseded'] as const; +export interface ContractDocument { + id: string; + version: string; + profile: string; + name: string; + summary: string; + requirements: string[]; + detailsSchema: Record; + operations: Operation[]; +} -/** A MAP problem a request earns before the service's own state is consulted. */ -export type RequestProblem = { code: ProblemCode; title: string; detail: string }; +export type CapabilityKind = keyof typeof CAPABILITY_KINDS; -const FIELD_VALIDATORS = 128; +/** The capability binding's kinds: the exact effects each declares and its longest lifetime. */ +export const CAPABILITY_KINDS = { + refusal: { effects: ['refusal', 'state'], maxLifetimeSeconds: 604800 }, + 'protective-report': { effects: ['protection', 'state'], maxLifetimeSeconds: 259200 }, + 'address-confirmation': { effects: ['assertion', 'state'], maxLifetimeSeconds: 86400 }, +} as const; -function frozen(value: T): T { - const copy = structuredClone(value); - const freeze = (item: unknown) => { - if (item && typeof item === 'object') { - Object.values(item).forEach(freeze); - Object.freeze(item); - } - }; - freeze(copy); - return copy; -} +// The JSON Schema 2020-12 keywords a contract's schemas may use. Every implementation +// enforces all of them alike; scope-resolved references and content annotations are absent. +const KEYWORDS = new Set([ + '$schema', '$ref', '$defs', '$comment', + 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas', + 'prefixItems', 'items', 'contains', 'properties', 'patternProperties', + 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', + 'type', 'enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', + 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', + 'uniqueItems', 'maxContains', 'minContains', 'maxProperties', 'minProperties', 'required', + 'dependentRequired', 'format', + 'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly', 'examples', +]); // prettier-ignore +const SCHEMA = ['additionalProperties', 'propertyNames', 'items', 'contains', 'not', 'if', 'then', 'else', 'unevaluatedItems', 'unevaluatedProperties']; // prettier-ignore +const SCHEMA_LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']; +const SCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'dependentSchemas']; +const DIALECT = 'https://json-schema.org/draft/2020-12/schema'; +// The keywords that apply their subschemas to the value itself, not to a member or item. +const IN_PLACE = new Set(['not', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'dependentSchemas']); -/** Each operation's branch of a request schema, with its input schema. */ -function branches(requestSchema: JsonObject) { - const last = ((requestSchema.allOf as JsonObject[] | undefined) ?? []).at(-1) ?? {}; - return ((last.oneOf as JsonObject[] | undefined) ?? [last]).map((branch) => { - const properties = (branch.properties ?? {}) as Record; - return { - operation: properties.operation?.const as string | undefined, - input: properties.input ?? {}, - }; - }); -} +const escape = (name: string) => name.replaceAll('~', '~0').replaceAll('/', '~1'); -/** A JSON Pointer to the failing member, naming a missing or extra member itself. */ -function pointerOf(error: ErrorObject) { - const name = error.params.missingProperty ?? error.params.additionalProperty; - return name === undefined ? error.instancePath : `${error.instancePath}/${escape(String(name))}`; +/** The values a schema object holds where schemas belong, as [keyword, pointer, value]. */ +function children(node: Record, pointer: string): [string, string, unknown][] { + const found: [string, string, unknown][] = []; + for (const keyword of SCHEMA) + if (Object.hasOwn(node, keyword)) found.push([keyword, `${pointer}/${keyword}`, node[keyword]]); + for (const keyword of SCHEMA_LISTS) + if (Array.isArray(node[keyword])) + for (const [index, item] of node[keyword].entries()) + found.push([keyword, `${pointer}/${keyword}/${index}`, item]); + for (const keyword of SCHEMA_MAPS) { + const map = node[keyword]; + if (map && typeof map === 'object' && !Array.isArray(map)) + for (const [name, item] of Object.entries(map)) + found.push([keyword, `${pointer}/${keyword}/${escape(name)}`, item]); + } + return found; } -/** Input errors as a detail and a pointer, each within the core limits, the first 100 kept. */ -function inputErrors(errors: ErrorObject[] | null | undefined, prefix = ''): InputError[] { - const seen = new Set(); - return (errors ?? []) - .filter((error) => error.keyword !== 'if') - .map((error) => inputError(error.message ?? 'invalid', `${prefix}${pointerOf(error)}`)) - .filter(({ detail, pointer }) => { - const key = JSON.stringify([pointer, detail]); - return !seen.has(key) && Boolean(seen.add(key)); - }) - .slice(0, 100); +const isObject = (value: unknown): value is Record => + !!value && typeof value === 'object' && !Array.isArray(value); + +/** + * Every schema within a schema, object or boolean, with its JSON Pointer from the root, the + * schema itself first. + */ +function* schemas(schema: unknown, pointer: string): Generator<[string, unknown]> { + if (typeof schema === 'boolean') yield [pointer, schema]; + if (!isObject(schema)) return; + yield [pointer, schema]; + for (const [, at, item] of children(schema, pointer)) yield* schemas(item, at); } /** - * A type contract an implementation vendors, verified against the digest it was pinned by - * and compiled once. It checks the descriptions, requests, inputs and results of that exact - * type. The Registry has already applied the contract rules, such as portable patterns, to - * the contract that digest names; loading refuses anything that would otherwise fail when a - * request arrives. + * Whether applying the schema can never finish: from its root, a chain of $ref and in-place + * keywords returns to a schema without moving into a member or item. */ -export class Contract { - readonly document: TypeContract; - readonly digest: string; - readonly requestSchema: JsonObject; - readonly #ajv = createValidator(); - readonly #details?: ValidateFunction; - readonly #request: ValidateFunction; - readonly #inputs = new Map(); - readonly #outputs = new Map(); - readonly #fields = new Map(); +function endless(schema: Record): boolean { + // Where each schema object sends its value, and whether it is the same value. + const links = new Map(); + for (const [at, node] of schemas(schema, '')) { + if (!isObject(node)) continue; + const applied = children(node, at) + .filter(([keyword]) => keyword !== '$defs') + .map(([keyword, to]): [string, boolean] => [to, IN_PLACE.has(keyword)]); + if (typeof node.$ref === 'string') applied.push([node.$ref.slice(1), true]); + links.set(at, applied); + } + const reachable = ['']; + for (const at of reachable) + for (const [to] of links.get(at) ?? []) if (!reachable.includes(to)) reachable.push(to); + const state = new Map(); + const loops = (at: string): boolean => { + if (state.has(at)) return state.get(at) === 'open'; + state.set(at, 'open'); + const looped = (links.get(at) ?? []).some(([to, inPlace]) => inPlace && loops(to)); + state.set(at, 'done'); + return looped; + }; + return reachable.some(loops); +} - /** - * `digest` is the contract digest the implementation pinned; `dependencies` supplies, by - * URL, any pinned schema the package does not bundle. - */ - constructor( - contract: unknown, - requestSchema: unknown, - { - digest: pinned, - dependencies = {}, - }: { digest: string; dependencies?: Record }, - ) { - const errors = contractFormatErrors(contract); - if (errors.length) throw new InvalidContract(`invalid type contract: ${errors.join('; ')}`); - this.document = frozen(contract as TypeContract); - if (this.document.profile !== MAP_PROFILE) - throw new InvalidContract('the contract is for another MAP profile'); - this.digest = digest(this.document); - if (this.digest !== pinned) - throw new InvalidContract(`the contract digest is ${this.digest}, not the pinned ${pinned}`); - this.requestSchema = frozen(requestSchema as JsonObject); - const references = this.#pin(dependencies); - const supplied = [...references] - .filter(([url]) => !BUNDLED.has(url)) - .map(([, schema]) => schema); - this.#verify(references, supplied); - try { - // Every schema compiles now, so a pattern or reference fails the contract, not a request. - for (const schema of [...supplied, this.requestSchema]) this.#ajv.addSchema(schema); - for (const schema of supplied) this.#ajv.getSchema(String(schema.$id)); - this.#request = this.#ajv.getSchema(String(this.requestSchema.$id))!; - if (this.document.detailsSchema) - this.#details = this.#ajv.compile(this.document.detailsSchema); - for (const branch of branches(this.requestSchema)) - this.#inputs.set(branch.operation!, this.#ajv.compile(branch.input)); - for (const operation of this.document.operations) - for (const result of operation.results) - this.#outputs.set( - `${operation.id}/${result.state}`, - this.#ajv.compile(result.outputSchema), - ); - } catch (error) { - throw new InvalidContract(`a schema does not compile: ${(error as Error).message}`); +/** Why a schema a contract supplies cannot be enforced alike everywhere, if it cannot. */ +function schemaProblems(schema: Record, pointer: string): string[] { + const problems: string[] = []; + const positions = new Set([...schemas(schema, '')].map(([at]) => at)); + for (const [at, value] of schemas(schema, pointer)) { + if (typeof value === 'boolean') continue; + const node = value as Record; + for (const keyword of Object.keys(node)) + if (!KEYWORDS.has(keyword)) problems.push(`${at}: ${keyword} is not a MAP schema keyword`); + if ('$schema' in node && (at !== pointer || node.$schema !== DIALECT)) + problems.push(`${at}: $schema names JSON Schema 2020-12, at the schema's root only`); + const ref = node.$ref; + if ( + ref !== undefined && + (typeof ref !== 'string' || !ref.startsWith('#') || !positions.has(ref.slice(1))) + ) + problems.push(`${at}: $ref is a JSON Pointer to a schema within this schema`); + if ('format' in node && !(FORMATS as readonly unknown[]).includes(node.format)) + problems.push(`${at}: format is one of ${FORMATS.join(', ')}`); + if (typeof node.multipleOf === 'number' && !Number.isInteger(node.multipleOf)) + problems.push(`${at}: multipleOf is an integer`); + const patterns = [ + ...(typeof node.pattern === 'string' ? [node.pattern] : []), + ...Object.keys((node.patternProperties as object | undefined) ?? {}), + ]; + for (const pattern of patterns) { + const problem = unportablePattern(pattern); + if (problem) problems.push(`${at}: pattern ${JSON.stringify(pattern)} uses ${problem}`); } } - - get id() { - return this.document.id; + if (problems.length) return problems; + if (endless(schema)) return [`${pointer}: $ref leads back without moving into the value`]; + try { + createValidator().compile(schema); + } catch (error) { + problems.push(`${pointer}: ${(error as Error).message}`); } + return problems; +} - get version() { - return this.document.version; - } +const validateContract = createValidator().compile(contractSchema); - /** The type reference a description and a request of this contract name exactly. */ - get typeReference(): TypeReference { - return { id: this.id, version: this.version, contractDigest: this.digest }; - } +/** An HTTPS URL's host and port as an origin compares them: lowercase, without :443. */ +const authority = (url: string) => + /^https:\/\/([^/?#]*)/i.exec(url)?.[1].toLowerCase().replace(/:443$/, ''); - operation(id: string) { - return this.document.operations.find((operation) => operation.id === id); - } - - /** Whether completing the operation decides the interaction. */ - isDecision(id: string) { - const found = this.operation(id); - if (!found) throw new TypeError(`${id} is not an operation of this type.`); - return !found.repeatable; +/** Why the value is not a MAP 0.3 type contract, or no reasons. */ +export function contractErrors(value: unknown): string[] { + if (!validateContract(value)) return schemaErrors(validateContract.errors); + const errors: string[] = []; + if (new TextEncoder().encode(JSON.stringify(value)).length > CONTRACT_MAX_BYTES) + errors.push(`/: exceeds ${CONTRACT_MAX_BYTES} bytes`); + const ids = value.operations.map((operation) => operation.id); + if (new Set(ids).size !== ids.length) errors.push('/operations: identifiers must be unique'); + errors.push(...schemaProblems(value.detailsSchema, '/detailsSchema')); + for (const [index, operation] of value.operations.entries()) { + const at = `/operations/${index}`; + if (operation.inputSchema) + errors.push(...schemaProblems(operation.inputSchema, `${at}/inputSchema`)); + if (!operation.capability) continue; + const kind = CAPABILITY_KINDS[operation.capability.kind]; + const expected = kind.effects.map((effect) => `${EFFECTS}${effect}`).sort(); + if (operation.effects.toSorted().join('\n') !== expected.join('\n')) + errors.push(`${at}/effects: a ${operation.capability.kind} capability declares exactly ${expected.join(' and ')}`); + if (operation.capability.maxLifetimeSeconds > kind.maxLifetimeSeconds) + errors.push(`${at}/capability/maxLifetimeSeconds: at most ${kind.maxLifetimeSeconds}`); } + return errors; +} - /** - * Every rule a description must satisfy beyond the core schema, for the service that issues - * it and the client that receives it. - */ - descriptionErrors(description: unknown): string[] { - const errors = descriptionErrors(description); - if (errors.length) return errors; - const issued = description as MapDescription; - if (!this.#names(issued.type)) return ['The description names another type contract.']; - const problems = [...this.#detailsProblems(issued), ...this.#operationProblems(issued)]; - if (!(Date.parse(issued.describedAt) < Date.parse(issued.expiresAt))) - problems.push('The interaction expires before it was described.'); - if (issued.service.authority === 'possession') problems.push(...capabilityProblems(issued)); - return problems; - } +/** A valid contract with its digest and compiled schemas. */ +export class Contract { + readonly document: ContractDocument; + /** `sha-256:` and the SHA-256 of the contract's RFC 8785 form. */ + readonly digest: string; + readonly #details: ValidateFunction; + readonly #inputs = new Map(); - /** - * The checks a service makes on a request once it has resolved the description it issued, - * compared the description digest and established the caller: the exact type, an offered - * operation the authority permits, and expiry. Undefined when they pass. The service then - * applies its own state (a decided interaction, a stale target) and `inputErrors`, in that - * order, before any effect. - */ - requestProblem( - description: MapDescription, - request: MapRequest, - { now }: { now: Date }, - ): RequestProblem | undefined { - if (!this.#names(request.type) || !this.#names(description.type)) - return { - code: 'unsupported-type', - title: 'Unsupported interaction type', - detail: 'The service does not implement this exact interaction type contract.', - }; - if (!this.#offered(description, request.operation)) - return { - code: 'unsupported-operation', - title: 'Unsupported operation', - detail: 'The operation was not offered in this interaction.', - }; - if (reached(now, description.expiresAt)) - return { - code: 'expired-interaction', - title: 'Interaction expired', - detail: 'The interaction expired before the request was processed.', - }; + /** The contract, or InvalidDocument with every reason the value is not one. */ + constructor(value: unknown) { + const errors = contractErrors(value); + if (errors.length) throw new InvalidDocument('The value is not a MAP 0.3 type contract.', errors); + this.document = structuredClone(value as ContractDocument); + this.digest = digest(this.document); + const ajv = createValidator(); + this.#details = ajv.compile(this.document.detailsSchema); + for (const operation of this.document.operations) + if (operation.inputSchema) this.#inputs.set(operation.id, ajv.compile(operation.inputSchema)); } - /** - * Input problems for one request, each with a detail and a JSON Pointer into its input: the - * operation's input schema, then its field bindings against the description's details. Type - * rules a contract cannot express are the caller's. - */ - inputErrors(description: MapDescription, request: MapRequest): InputError[] { - const found = this.operation(request.operation); - const validate = this.#inputs.get(request.operation); - if (!found || !validate) return [inputError('The operation is not part of this type.', '')]; - if (!validate(request.input)) return inputErrors(validate.errors); - return (found.fieldBindings ?? []) - .flatMap((binding) => { - const fields = member(description.details, binding.fields) as JsonObject | undefined; - const supplied = member(request.input, binding.input); - if (!fields) - return supplied === undefined - ? [] - : [inputError('This interaction defines no fields for these values.', binding.input)]; - if (supplied === undefined) - return ((fields.required ?? []) as string[]).length - ? [inputError('Values for the required fields are missing.', binding.input)] - : []; - const schema = this.#fieldValues(fields); - return schema(supplied) ? [] : inputErrors(schema.errors, binding.input); - }) - .slice(0, 100); + /** The contract in the bytes or text, read as MAP JSON within the contract limit. */ + static parse(input: string | Uint8Array): Contract { + return new Contract(parse(input, CONTRACT_MAX_BYTES)); } - /** A request against the core request definition and this contract's request schema. */ - requestErrors(request: unknown): string[] { - return [ - ...requestErrors(request), - ...(this.#request(request) ? [] : errorList(this.#request.errors)), - ]; - } - - /** The core result definition, then the output schema and reason the operation declares. */ - resultErrors(result: unknown): string[] { - const errors = resultErrors(result); - if (errors.length) return errors; - const recorded = result as MapResult; - if (!this.#names(recorded.type)) return ['The result names another type contract.']; - const found = this.operation(recorded.operation); - if (!found) return [`${recorded.operation} is not an operation of this type.`]; - const declared = found.results.find((entry) => entry.state === recorded.state); - if (!declared) return [`${found.id} does not declare the state ${recorded.state}.`]; - const validate = this.#outputs.get(`${found.id}/${declared.state}`)!; - const problems = validate(recorded.output) ? [] : errorList(validate.errors, '/output'); - if (recorded.state === 'failed' && !declared.reasons?.includes(recorded.reason!)) - problems.push(`${found.id} does not declare the reason ${recorded.reason}.`); - return problems; + get id() { + return this.document.id; } - #names(type: TypeReference) { - return ( - type.id === this.id && type.version === this.version && type.contractDigest === this.digest - ); + get version() { + return this.document.version; } - #offered(description: MapDescription, id: string) { - return ( - description.operations.some((offered) => offered.id === id) && - Boolean(this.operation(id)?.authority.includes(description.service.authority)) - ); + operation(id: string): Operation | undefined { + return this.document.operations.find((operation) => operation.id === id); } - #operationProblems(description: MapDescription) { - const ids = description.operations.map((offered) => offered.id); - const authority = description.service.authority; - return [ - ...(new Set(ids).size === ids.length ? [] : ['An operation is offered twice.']), - ...ids - .filter((id) => !this.operation(id)?.authority.includes(authority)) - .map((id) => `${id} is not a ${authority} operation of this type.`), - ]; + /** Why the details do not satisfy the contract's details schema, or no reasons. */ + detailsErrors(details: unknown): string[] { + return this.#details(details) ? [] : schemaErrors(this.#details.errors, '/details'); } - #detailsProblems(description: MapDescription) { - const present = Object.hasOwn(description, 'details'); - const valid = this.#details ? present && this.#details(description.details) : !present; - if (!valid) return ['The details do not satisfy the type contract.']; - const blocks = new Set( - this.document.operations.flatMap((operation) => - (operation.fieldBindings ?? []).map((binding) => binding.fields), - ), - ); - return [...blocks].flatMap((pointer) => { - const fields = member(description.details, pointer) as JsonObject | undefined; - return fields ? formProblems(fields).map((problem) => `${pointer}: ${problem}`) : []; - }); + /** Why the input is not acceptable for the operation, or no reasons. */ + inputErrors(operationId: string, input: unknown): string[] { + const operation = this.operation(operationId); + if (!operation) return [`/operation: ${operationId} is not an operation of this contract`]; + const validate = this.#inputs.get(operationId); + if (!validate) return input === undefined ? [] : ['/input: this operation accepts no input']; + return validate(input) ? [] : schemaErrors(validate.errors, '/input'); } /** - * The compiled schema of a fields block's values. Least recently used blocks leave first, - * with their compiled copies, so a long-running service stays bounded. + * Why a valid description does not use this contract as it allows, or no reasons: it names + * this contract by identifier, version and digest; it offers only declared operations; a + * capability appears only where the contract permits one, at the service's origin and + * within the kind's lifetime; and its details satisfy the details schema. */ - #fieldValues(fields: JsonObject) { - const key = digest(fields); - let entry = this.#fields.get(key); - if (entry) this.#fields.delete(key); - else { - const schema = fieldValuesSchema(fields); - entry = { schema, validate: this.#ajv.compile(schema) }; - } - this.#fields.set(key, entry); - if (this.#fields.size > FIELD_VALIDATORS) { - const [oldest, evicted] = this.#fields.entries().next().value!; - this.#fields.delete(oldest); - this.#ajv.removeSchema(evicted.schema); - } - return entry.validate; - } - - /** The pinned schemas by URL, bundled or supplied, each matching its pinned digest. */ - #pin(dependencies: Record) { - const references = new Map(); - for (const dependency of this.document.dependencies ?? []) { - const schema = (BUNDLED.get(dependency.url) ?? dependencies[dependency.url]) as - JsonObject | undefined; - if (!schema) throw new InvalidContract(`unknown dependency ${dependency.url}`); - if (digest(schema) !== dependency.canonicalDigest) - throw new InvalidContract(`the pinned digest of ${dependency.url} differs`); - if (schema.$id !== dependency.url) - throw new InvalidContract(`the schema pinned as ${dependency.url} has another $id`); - references.set(dependency.url, BUNDLED.has(dependency.url) ? schema : frozen(schema)); - } - if (!references.has(CORE_SCHEMA)) throw new InvalidContract('the core schema must be pinned'); - return references; - } - - #verify(references: Map, supplied: JsonObject[]) { - const inline = [ - this.document.detailsSchema, - ...this.document.operations.flatMap((operation) => - operation.results.map((result) => result.outputSchema), - ), - ].filter(Boolean); - // Every schema is valid JSON Schema 2020-12, so no malformed keyword fails at request time. - for (const schema of [...inline, this.requestSchema, ...supplied]) - if (!this.#ajv.validateSchema(schema as JsonObject)) - throw new InvalidContract( - `a schema is not valid JSON Schema 2020-12: ${this.#ajv.errorsText(this.#ajv.errors)}`, - ); - const reference = this.document.requestSchema; - if (this.requestSchema.$schema !== JSON_SCHEMA_2020_12) - throw new InvalidContract('the request schema must declare JSON Schema 2020-12'); - if (this.requestSchema.$id !== reference.url) - throw new InvalidContract('the request schema $id differs from the contract'); - if (digest(this.requestSchema) !== reference.canonicalDigest) - throw new InvalidContract('the request schema digest differs from the contract'); - const constants = new Set(values(this.requestSchema, 'const')); - const ids = this.document.operations.map((operation) => operation.id); - for (const expected of [this.id, this.version, ...ids]) - if (!constants.has(expected)) - throw new InvalidContract(`the request schema does not bind ${expected}`); - if (new Set(ids).size !== ids.length) - throw new InvalidContract('duplicate operation identifiers'); - for (const operation of this.document.operations) { - const states = operation.results.map((result) => result.state); - if (new Set(states).size !== states.length) - throw new InvalidContract(`duplicate ${operation.id} result states`); + descriptionErrors(description: Description): string[] { + const invalid = descriptionErrors(description); + if (invalid.length) return invalid; + const errors: string[] = []; + const { type } = description; + if (type.id !== this.id || type.version !== this.version) + errors.push(`/type: names ${type.id} ${type.version}, not this contract`); + if (type.contractDigest !== this.digest) errors.push('/type/contractDigest: does not match'); + if (description.profile !== this.document.profile) + errors.push('/profile: differs from the contract profile'); + const lifetime = (Date.parse(description.expiresAt) - Date.parse(description.issuedAt)) / 1000; + const origin = authority(description.service.id); + for (const [index, offered] of description.operations.entries()) { + const at = `/operations/${index}`; + const operation = this.operation(offered.id); + if (!operation) { + errors.push(`${at}/id: ${offered.id} is not an operation of this contract`); + continue; + } + if (!offered.capability) continue; + if (!operation.capability) + errors.push(`${at}/capability: the contract permits no capability for ${offered.id}`); + else if (lifetime > operation.capability.maxLifetimeSeconds) + errors.push(`/expiresAt: a ${offered.id} capability lasts at most ${operation.capability.maxLifetimeSeconds} seconds`); + if (authority(offered.capability.url) !== origin) + errors.push(`${at}/capability/url: must have the service's origin`); } - const problem = referenceProblem(inline, this.requestSchema, references, supplied); - if (problem) throw new InvalidContract(problem); + return [...errors, ...this.detailsErrors(description.details)]; } } diff --git a/src/core/description.ts b/src/core/description.ts new file mode 100644 index 0000000..9e9e8eb --- /dev/null +++ b/src/core/description.ts @@ -0,0 +1,68 @@ +// A MAP description: the JSON-LD a message carries beside its readable content. +import { coreSchema, createValidator, DESCRIPTION_MAX_BYTES, schemaErrors } from './artifacts.ts'; +import { InvalidDocument, parse } from './json.ts'; + +export interface Description { + '@context': string; + '@type': 'MailAction'; + '@id': string; + profile: string; + type: { id: string; version: string; contractDigest: string }; + issuedAt: string; + expiresAt: string; + inLanguage: string; + service: { id: string; tenant?: string }; + recipient: string; + subject: { id: string; title: string }; + terms: { id: string; version: string }; + details: Record; + operations: { id: string; capability?: { url: string } }[]; + human: { url: string }; +} + +const validate = createValidator().compile(coreSchema); + +// Whitespace, controls, bidirectional and invisible formatting characters. +const UNSAFE = /[\p{White_Space}\p{Cc}\p{Cf}]/u; + +/** + * Why the value is not a MAP 0.3 description, or no reasons. Beyond the core schema: + * expiry follows issuance, operation identifiers are unique, and no security-sensitive + * identifier contains whitespace, a control or a formatting character. + */ +export function descriptionErrors(value: unknown): string[] { + if (!validate(value)) return schemaErrors(validate.errors); + const errors: string[] = []; + if (Date.parse(value.expiresAt) <= Date.parse(value.issuedAt)) + errors.push('/expiresAt: must be later than issuedAt'); + const ids = value.operations.map((operation) => operation.id); + if (new Set(ids).size !== ids.length) errors.push('/operations: identifiers must be unique'); + const identifiers: [string, string | undefined][] = [ + ['/@id', value['@id']], + ['/type/id', value.type.id], + ['/service/id', value.service.id], + ['/service/tenant', value.service.tenant], + ['/recipient', value.recipient], + ['/subject/id', value.subject.id], + ['/terms/id', value.terms.id], + ['/human/url', value.human.url], + ...value.operations.map( + (operation, index): [string, string | undefined] => [ + `/operations/${index}/capability/url`, + operation.capability?.url, + ], + ), + ]; + for (const [pointer, identifier] of identifiers) + if (identifier !== undefined && UNSAFE.test(identifier)) + errors.push(`${pointer}: must not contain whitespace, controls or formatting characters`); + return errors; +} + +/** The description in the bytes or text, or InvalidDocument with every reason it is not one. */ +export function parseDescription(input: string | Uint8Array): Description { + const value = parse(input, DESCRIPTION_MAX_BYTES); + const errors = descriptionErrors(value); + if (errors.length) throw new InvalidDocument('The value is not a MAP 0.3 description.', errors); + return value as Description; +} diff --git a/src/core/documents.ts b/src/core/documents.ts deleted file mode 100644 index ce45c8d..0000000 --- a/src/core/documents.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Building MAP results and problems, and the lifecycle rules every service applies. -import { MAP_PROFILE, problemErrors, reached, resultErrors } from './artifacts.ts'; -import { problemMembers, withinDocument } from './limits.ts'; -import type { - InputError, - JsonObject, - MapDescription, - MapProblem, - MapRequest, - MapResult, - ResultState, - Target, -} from './types.ts'; - -/** The HTTP status of every MAP problem code, as the core schema fixes it. */ -export const PROBLEM_STATUS = { - 'invalid-request': 400, - 'authentication-required': 401, - refused: 403, - 'result-not-found': 404, - 'stale-target': 409, - 'idempotency-conflict': 409, - 'request-in-progress': 409, - 'already-decided': 409, - 'expired-interaction': 410, - 'unsupported-type': 422, - 'unsupported-operation': 422, -} as const; -export type ProblemCode = keyof typeof PROBLEM_STATUS; - -const PROBLEM_TYPES = 'https://mailschema.org/problems/'; -const NON_TERMINAL: readonly ResultState[] = ['pending', 'approval-required']; - -/** The document, once the core definition `check` names accepts it. */ -function built(document: JsonObject, check: (value: unknown) => string[]): T { - const refused = check(document); - if (refused.length) - throw new TypeError(`The core refuses this document: ${refused.slice(0, 3).join('; ')}`); - return document as T; -} - -const defined = (document: JsonObject) => - Object.fromEntries(Object.entries(document).filter(([, value]) => value !== undefined)); - -/** A result with its members in one order, whichever builder made it, checked by the core. */ -const resultDocument = (members: Omit): MapResult => - built( - defined({ - kind: 'MapResult', - profile: MAP_PROFILE, - requestId: members.requestId, - interactionId: members.interactionId, - descriptionDigest: members.descriptionDigest, - type: members.type, - operation: members.operation, - state: members.state, - target: members.target, - recordedAt: members.recordedAt, - resultUrl: members.resultUrl, - actor: members.actor, - approvalUrl: members.approvalUrl, - reason: members.reason, - output: members.output, - }), - resultErrors, - ); - -/** - * A result for a request, with every correlation member. `approvalUrl` is given exactly for - * approval-required and `reason` exactly for failed. Like every builder here, it throws - * rather than return a document the core refuses. - */ -export function result( - request: MapRequest, - { - state, - target, - resultUrl, - recordedAt, - output = {}, - reason, - approvalUrl, - actor, - }: { - state: ResultState; - target: Target; - resultUrl: string; - recordedAt: Date; - output?: JsonObject; - reason?: string; - approvalUrl?: string; - actor?: string; - }, -): MapResult { - if ((state === 'approval-required') !== (approvalUrl !== undefined)) - throw new TypeError('approvalUrl is given exactly when the state is approval-required.'); - if ((state === 'failed') !== (reason !== undefined)) - throw new TypeError('reason is given exactly when the state is failed.'); - return resultDocument({ - requestId: request.requestId, - interactionId: request.interactionId, - descriptionDigest: request.descriptionDigest, - type: request.type, - operation: request.operation, - state, - target, - recordedAt: recordedAt.toISOString(), - resultUrl, - actor, - approvalUrl, - reason, - output, - }); -} - -/** - * The next state of a pending or approval-required result. Every correlation member and the - * actor stay; the state, reason, output and recording time change. - */ -export function transition( - previous: MapResult, - { - state, - recordedAt, - reason, - output = {}, - }: { state: ResultState; recordedAt: Date; reason?: string; output?: JsonObject }, -): MapResult { - if (!NON_TERMINAL.includes(previous.state)) - throw new TypeError(`A ${previous.state} result is terminal.`); - if (NON_TERMINAL.includes(state)) throw new TypeError('A transition ends pending work.'); - if ((state === 'failed') !== (reason !== undefined)) - throw new TypeError('reason is given exactly when the state is failed.'); - return resultDocument({ - ...previous, - approvalUrl: undefined, - state, - reason, - output, - recordedAt: recordedAt.toISOString(), - }); -} - -/** - * A problem. With `requestId` it is correlated and carries `instance`, `profile`, - * `requestId`, `interactionId` and `code`; without, it is plain RFC 9457. It stays within - * the core limits: a long detail is cut, each input error is bounded, and only as many of - * the first 100 errors as fit within 64 KiB are kept. - */ -export function problem( - code: ProblemCode, - { - title, - detail, - requestId, - interactionId, - resultUrl, - target, - errors, - }: { - title: string; - detail: string; - requestId?: string; - interactionId?: string; - resultUrl?: string; - target?: Target; - errors?: InputError[]; - }, -): MapProblem { - const members = problemMembers(title, detail, errors); - if (interactionId && !requestId) - throw new TypeError('An interaction is named only with its request.'); - if (requestId && code === 'authentication-required') - throw new TypeError('authentication-required is never correlated.'); - if (members.errors && !(requestId && code === 'invalid-request')) - throw new TypeError('Errors belong to a correlated invalid-request.'); - let correlation: JsonObject = {}; - if (requestId) { - if (!resultUrl) throw new TypeError('A correlated problem names its result URL.'); - if (!interactionId && code !== 'result-not-found') - throw new TypeError('A correlated problem names its interaction.'); - if (code === 'stale-target' && !target) - throw new TypeError('A stale-target problem carries the current target.'); - correlation = { instance: resultUrl, profile: MAP_PROFILE, requestId, interactionId, code }; - } - return built( - withinDocument( - defined({ - type: `${PROBLEM_TYPES}${code}`, - title: members.title, - status: PROBLEM_STATUS[code], - detail: members.detail, - ...correlation, - target, - errors: members.errors, - }), - ), - problemErrors, - ); -} - -/** The HTTP status of a result: 202 while work is pending, otherwise 200. */ -export const resultStatus = (document: MapResult) => - NON_TERMINAL.includes(document.state) ? 202 : 200; - -/** - * Results stay retrievable until the later of the interaction's expiry and the retention - * interval measured from the latest recorded state. - */ -export const retainUntil = (description: MapDescription, recordedAt: Date) => - new Date( - Math.max( - Date.parse(description.expiresAt), - recordedAt.getTime() + description.service.execution.resultRetentionSeconds * 1000, - ), - ); - -/** - * An undecided approval ends as failed, with reason expired, at the interaction's expiry, - * whether or not anyone looks. The settled result, or undefined when nothing changes. - */ -export function settle(document: MapResult, description: MapDescription, now: Date) { - if (document.state !== 'approval-required' || !reached(now, description.expiresAt)) return; - return transition(document, { - state: 'failed', - reason: 'expired', - recordedAt: new Date(description.expiresAt), - }); -} diff --git a/src/core/implementation.ts b/src/core/implementation.ts new file mode 100644 index 0000000..29a2189 --- /dev/null +++ b/src/core/implementation.ts @@ -0,0 +1,45 @@ +// An implementation record: a Registry declaration or report of a service's support for exact +// contract versions. +import { + CONTRACT_MAX_BYTES, + createValidator, + implementationSchema, + schemaErrors, +} from './artifacts.ts'; +import { InvalidDocument, parse } from './json.ts'; + +export interface ImplementationRecord { + service: string; + maintainer: { name: string; url: string }; + type: { id: string; version: string; contractDigest: string }; + operations: string[]; + binding: string; + artifact?: { url: string; digest: string; digestMode: 'canonical-json' | 'bytes' }; + format?: string; + status: 'Draft' | 'Experimental' | 'Stable' | 'Deprecated' | 'Superseded'; + documentation: string; + evidence: { kind: 'declaration' | 'test-report'; url: string; summary: string }[]; + successor?: string; +} + +const validate = createValidator().compile(implementationSchema); + +/** Why the value is not an implementation record, or no reasons. */ +export function implementationErrors(value: unknown): string[] { + if (!validate(value)) return schemaErrors(validate.errors); + const errors: string[] = []; + const { origin } = new URL(value.service); + if (value.service !== origin && value.service !== `${origin}/`) + errors.push('/service: must be an HTTPS origin, not an operation path'); + if (new Set(value.operations).size !== value.operations.length) + errors.push('/operations: identifiers must be unique'); + return errors; +} + +/** The record in the bytes or text, or InvalidDocument with every reason it is not one. */ +export function parseImplementation(input: string | Uint8Array): ImplementationRecord { + const value = parse(input, CONTRACT_MAX_BYTES); + const errors = implementationErrors(value); + if (errors.length) throw new InvalidDocument('The value is not an implementation record.', errors); + return value as ImplementationRecord; +} diff --git a/src/core/index.ts b/src/core/index.ts index ee64cd7..44cded9 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,54 +1,31 @@ -// Mail Action Protocol 0.2 processing. It parses, canonicalizes, digests and validates MAP -// documents, checks the type contracts an implementation vendors, and builds results and -// problems. It does not establish endpoint trust, verify email authentication, grant -// authority or send email. +// Mail Action Protocol 0.3 processing: one implementation, run unchanged by the repository's +// site and checks and shipped as the npm package. export { - MAP_PROFILE, - CORE_SCHEMA, - FORMS_SCHEMA, - CONTRACT_FORMAT, + PROFILE, CONTEXT, - mapErrors, - descriptionErrors, - requestErrors, - resultErrors, - problemErrors, - isRequestId, - reached, + DESCRIPTION_MEDIA_TYPE, + DESCRIPTION_MAX_BYTES, + CONTRACT_MAX_BYTES, + EFFECTS, + FORMATS, + coreSchema, + contractSchema, + implementationSchema, } from './artifacts.ts'; -export { InvalidDocument, parse, canonicalize, digest } from './document.ts'; -export { APPROVAL_REASONS, Contract, InvalidContract, type RequestProblem } from './contract.ts'; +export { InvalidDocument, MAX_DEPTH, decodeUtf8, parse, canonicalize, digest } from './json.ts'; +export { sha256 } from './sha256.ts'; +export { descriptionErrors, parseDescription, type Description } from './description.ts'; export { - PROBLEM_STATUS, - type ProblemCode, - result, - transition, - problem, - resultStatus, - retainUntil, - settle, -} from './documents.ts'; + CAPABILITY_KINDS, + Contract, + contractErrors, + type CapabilityKind, + type ContractDocument, + type Operation, +} from './contract.ts'; export { - DESCRIPTION_MEDIA_TYPE, - isDescriptionPart, - capability, - writtenPath, - isJsonRequest, - resultUrl, -} from './binding.ts'; -export type { - Authority, - Consequence, - ContractOperation, - InputError, - JsonObject, - MapDescription, - MapProblem, - MapRequest, - MapResult, - ResultState, - SchemaReference, - Target, - TypeContract, - TypeReference, -} from './types.ts'; + implementationErrors, + parseImplementation, + type ImplementationRecord, +} from './implementation.ts'; +export { unportablePattern } from './patterns.ts'; diff --git a/src/core/document.ts b/src/core/json.ts similarity index 53% rename from src/core/document.ts rename to src/core/json.ts index 51f901d..6cc79ec 100644 --- a/src/core/document.ts +++ b/src/core/json.ts @@ -1,47 +1,57 @@ -// Parsing MAP documents as I-JSON within the core limits. -import { createHash } from 'node:crypto'; +// MAP JSON: I-JSON parsed from its original text, and the RFC 8785 digests that identify +// contracts and descriptions. +import { sha256 } from './sha256.ts'; -/** A MAP document that is not I-JSON within the core limits. */ -export class InvalidDocument extends Error {} +/** A document that is not MAP JSON, or not the document it claims to be. */ +export class InvalidDocument extends Error { + readonly errors: string[]; + constructor(message: string, errors: string[] = [message]) { + super(message); + this.errors = errors; + } +} -export const MAX_BYTES = 64 * 1024; export const MAX_DEPTH = 32; +const MAX_NUMBER_TOKEN = 64; const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? `\\u{${(plane * 0x10000 + 0xfffe).toString(16)}}\\u{${(plane * 0x10000 + 0xffff).toString(16)}}`).join('')}]`, - 'u', -); +const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/; +const ZERO = /^-?0(?:\.0+)?(?:[eE][+-]?[0-9]+)?$/; + +const forbidden = (point: number) => + point === 0 || (point >= 0xfdd0 && point <= 0xfdef) || (point & 0xfffe) === 0xfffe; + +/** The bytes as text, refusing anything but well-formed UTF-8. */ +export function decodeUtf8(input: Uint8Array): string { + try { + // The byte order mark is kept, so parsing refuses it. + return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(input); + } catch { + throw new InvalidDocument('The JSON is not valid UTF-8.'); + } +} /** - * Parse a MAP document as I-JSON (RFC 7493) within the core limits: valid UTF-8 with no - * byte order mark, at most 64 KiB, no duplicate member names, no lone surrogates, - * noncharacters or U+0000 in any string, every number within ±(2^53−1), and arrays and - * objects nested at most 32 deep, the outermost counting as one. Text is measured in its - * UTF-8 bytes; bytes must be strict UTF-8. + * Parse MAP JSON from its original text: UTF-8 I-JSON (RFC 7493) of at most `maxBytes`, with + * no byte order mark, duplicate member names, lone surrogates, noncharacters or U+0000, no + * container deeper than 32 counting the root as 1, and every number a finite token of at most + * 64 characters within ±(2^53−1) that does not underflow to zero. These rules apply to the + * original tokens, before a general parser could erase them. */ -export function parse(input: string | Uint8Array): unknown { - let text: string; - if (typeof input === 'string') text = input; - else { - try { - // The byte order mark is kept, so the parser refuses it. - text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(input); - } catch { - throw new InvalidDocument('The MAP document is not valid UTF-8.'); - } - } - if (new TextEncoder().encode(text).length > MAX_BYTES) - throw new InvalidDocument(`The MAP document exceeds ${MAX_BYTES} bytes.`); +export function parse(input: string | Uint8Array, maxBytes: number): unknown { + const text = typeof input === 'string' ? input : decodeUtf8(input); + if (text.charCodeAt(0) === 0xfeff) throw new InvalidDocument('A byte order mark is not allowed.'); + if (new TextEncoder().encode(text).length > maxBytes) + throw new InvalidDocument(`The JSON exceeds ${maxBytes} bytes.`); + let index = 0; - const fail = (message: string): never => { - throw new InvalidDocument(`Invalid MAP JSON at offset ${index}: ${message}`); + const fail = (reason: string): never => { + throw new InvalidDocument(`Invalid MAP JSON at offset ${index}: ${reason}.`); }; const space = () => { while (' \t\n\r'.includes(text[index] ?? '-')) index += 1; }; - const string = () => { + const string = (): string => { const start = index; index += 1; while (index < text.length && text[index] !== '"') index += text[index] === '\\' ? 2 : 1; @@ -54,11 +64,11 @@ export function parse(input: string | Uint8Array): unknown { return fail('malformed string'); } if (LONE_SURROGATE.test(value)) fail('lone surrogate'); - if (value.includes('\u0000')) fail('U+0000 in a string'); - if (NONCHARACTER.test(value)) fail('a noncharacter in a string'); + for (const character of value) + if (forbidden(character.codePointAt(0)!)) fail('U+0000 or a noncharacter'); return value; }; - // `depth` counts the arrays and objects enclosing the value. + // `depth` counts the containers enclosing the value. const value = (depth: number): unknown => { space(); const next = text[index]; @@ -124,19 +134,21 @@ export function parse(input: string | Uint8Array): unknown { } } if (next === '"') return string(); - const literal = - /^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/.exec( - text.slice(index), - ); - if (!literal) fail('unexpected token'); - index += literal![0].length; - const parsed = JSON.parse(literal![0]) as unknown; - if ( - typeof parsed === 'number' && - (!Number.isFinite(parsed) || Math.abs(parsed) > Number.MAX_SAFE_INTEGER) - ) - fail('number outside the I-JSON range'); - return parsed; + for (const literal of ['true', 'false', 'null'] as const) + if (text.startsWith(literal, index)) { + index += literal.length; + return JSON.parse(literal) as unknown; + } + const token = NUMBER.exec(text.slice(index))?.[0]; + if (token === undefined) return fail('unexpected token'); + index += token.length; + if (token.length > MAX_NUMBER_TOKEN) + fail(`a number token longer than ${MAX_NUMBER_TOKEN} characters`); + const number = Number(token); + if (!Number.isFinite(number) || Math.abs(number) > Number.MAX_SAFE_INTEGER) + fail('a number outside the I-JSON range'); + if (number === 0 && !ZERO.test(token)) fail('a number that underflows to zero'); + return number; }; const result = value(0); space(); @@ -156,8 +168,7 @@ const ESCAPES: Record = { /** * The RFC 8785 canonical form of a JSON value: members sorted by their UTF-16 code units, - * ECMAScript's minimal string escapes and its number format. An undefined member is - * left out and an undefined array item is null, as JSON.stringify writes them. + * ECMAScript's minimal string escapes and its number format. */ export function canonicalize(value: unknown): string { if (value === null || typeof value === 'boolean') return String(value); @@ -167,12 +178,10 @@ export function canonicalize(value: unknown): string { } if (typeof value === 'string') return `"${value.replace(/["\\\u0000-\u001f]/g, (char) => ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)}"`; - if (Array.isArray(value)) - return `[${value.map((item) => (item === undefined ? 'null' : canonicalize(item))).join(',')}]`; + if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`; if (typeof value === 'object') { const members = value as Record; return `{${Object.keys(members) - .filter((name) => members[name] !== undefined) .sort() .map((name) => `${canonicalize(name)}:${canonicalize(members[name])}`) .join(',')}}`; @@ -180,9 +189,5 @@ export function canonicalize(value: unknown): string { throw new TypeError(`${typeof value} is not a JSON value.`); } -/** - * `sha-256:` and the SHA-256 of the RFC 8785 canonical form: the digest MAP uses for - * descriptions, contracts and pinned schemas. - */ -export const digest = (value: unknown) => - `sha-256:${createHash('sha256').update(canonicalize(value)).digest('hex')}`; +/** `sha-256:` and the SHA-256 of the value's RFC 8785 form, as MAP identifies contracts. */ +export const digest = (value: unknown) => `sha-256:${sha256(canonicalize(value))}`; diff --git a/src/core/limits.ts b/src/core/limits.ts deleted file mode 100644 index 8d4d5ca..0000000 --- a/src/core/limits.ts +++ /dev/null @@ -1,57 +0,0 @@ -// MAP documents within the core limits, with lengths counted in code points as JSON -// Schema counts them. -import { MAX_BYTES } from './document.ts'; -import { codePoints, within } from './pointer.ts'; -import type { InputError, JsonObject } from './types.ts'; - -const TITLE = 240; -const DETAIL = 4000; -const POINTER = 1000; -const ERROR_DETAIL = 2000; -const ERRORS = 100; - -/** The text, or its first limit - 1 code points and an ellipsis. */ -export const cut = (text: string, limit: number) => - codePoints(text) > limit ? `${[...text].slice(0, limit - 1).join('')}…` : text; - -/** - * An input error within the core problem's limits. A pointer too long to report names its - * nearest ancestor that fits, with a detail that says so, and a detail too long is cut. - */ -export function inputError(detail: string, pointer: string): InputError { - if (codePoints(pointer) > POINTER) - return { - detail: 'A member within this value does not satisfy the contract.', - pointer: within(pointer, POINTER), - }; - return { detail: cut(detail, ERROR_DETAIL), pointer }; -} - -/** - * A problem's title, detail and input errors within the core limits. An empty or overlong - * title, an empty detail or an empty error list is the caller's mistake. - */ -export function problemMembers(title: string, detail: string, errors?: InputError[]) { - if (codePoints(title) < 1 || codePoints(title) > TITLE) - throw new RangeError(`A problem title is 1 to ${TITLE} characters.`); - if (!detail) throw new RangeError('A problem detail is not empty.'); - if (errors) { - if (!errors.length) throw new RangeError('Errors, when given, name at least one input error.'); - if (errors.some((error) => !error.detail)) throw new RangeError('An input error has a detail.'); - } - return { - title, - detail: cut(detail, DETAIL), - errors: errors?.slice(0, ERRORS).map((error) => inputError(error.detail, error.pointer)), - }; -} - -/** The problem with as many of its errors, in order, as fit within the document limit. */ -export function withinDocument(problem: JsonObject): JsonObject { - const errors = problem.errors as InputError[] | undefined; - if (!errors) return problem; - const kept = [...errors]; - const size = () => new TextEncoder().encode(JSON.stringify({ ...problem, errors: kept })).length; - while (kept.length > 1 && size() > MAX_BYTES) kept.pop(); - return { ...problem, errors: kept }; -} diff --git a/src/core/patterns.ts b/src/core/patterns.ts new file mode 100644 index 0000000..a037b2c --- /dev/null +++ b/src/core/patterns.ts @@ -0,0 +1,135 @@ +// The regular expressions a contract's schemas may use: the subset that ECMA-262 (with the +// u flag) and other engines read alike, so every implementation enforces the same contract. + +const SYNTAX_CHARACTERS = '^$\\.*+?()[]{}|/'; +const CONTROL_ESCAPES: Record = { t: 0x09, n: 0x0a, f: 0x0c, r: 0x0d }; +const QUANTIFIERS = '*+?{'; + +/** + * Why a pattern is outside the portable subset, or undefined. The subset is printable ASCII + * text, with any other code point written as \uXXXX. It has literal characters and escaped + * syntax characters; \t, \n, \f, \r and \d, the ASCII digits; non-empty classes of literal or + * escaped members and ranges, with a literal hyphen only first or last; (?: groups; + * alternation; ^ and $; and one *, +, ?, {n}, {n,} or {n,m} after an atom. Other class + * escapes and the dot match different characters in different engines, and brackets, && or a + * stray brace inside a class, or a quantifier on a quantifier, are read differently or refused + * by one of them. ^ and $ anchor the whole value, as in ECMA-262; an engine whose anchors also + * match at line breaks must read them so. + */ +export function unportablePattern(pattern: string): string | undefined { + if (!/^[\x20-\x7e]*$/.test(pattern)) return 'a character outside printable ASCII'; + let at = 0; + + // One escaped code point, the digit class outside a class, or a problem. + const escape = (inClass: boolean): number | string => { + const next = pattern[at + 1]; + if (next === undefined) return 'a trailing backslash'; + if (next === 'u') { + const digits = pattern.slice(at + 2, at + 6); + if (!/^[0-9a-fA-F]{4}$/.test(digits)) return 'a malformed \\u escape'; + const code = parseInt(digits, 16); + if (code >= 0xd800 && code <= 0xdfff) return 'a surrogate \\u escape'; + at += 6; + return code; + } + at += 2; + if (next === 'd' && !inClass) return -1; + if (Object.hasOwn(CONTROL_ESCAPES, next)) return CONTROL_ESCAPES[next]; + if (SYNTAX_CHARACTERS.includes(next) || (inClass && next === '-')) return next.charCodeAt(0); + return `the escape \\${next}`; + }; + + const characterClass = (): string | undefined => { + at += pattern[at + 1] === '^' ? 2 : 1; + // Each member's code point, or null for an unescaped hyphen. + const members: (number | null)[] = []; + while (pattern[at] !== ']') { + const char = pattern[at]; + if (char === undefined) return 'an unclosed character class'; + if (char === '[') return 'a bracket inside a class'; + if (char === '&' && pattern[at + 1] === '&') return '&& inside a class'; + if (char === '\\') { + const code = escape(true); + if (typeof code === 'string') return code; + members.push(code); + } else { + members.push(char === '-' ? null : char.charCodeAt(0)); + at += 1; + } + } + at += 1; + if (!members.length) return 'an empty character class'; + for (let index = 0; index < members.length; index += 1) { + const low = members[index]; + if (low === null) { + if (index !== 0 && index !== members.length - 1) return 'an ambiguous hyphen in a class'; + continue; + } + if (members[index + 1] !== null || index + 2 >= members.length) continue; + const high = members[index + 2]; + if (high === null) return 'an ambiguous hyphen in a class'; + if (high < low) return 'a class range out of order'; + index += 2; + if (members[index + 1] === null && index + 1 !== members.length - 1) + return 'an ambiguous hyphen in a class'; + } + }; + + const quantifier = (): string | undefined => { + if (pattern[at] === '{') { + const bounds = /^\{([0-9]{1,4})(?:(,)([0-9]{1,4})?)?\}/.exec(pattern.slice(at)); + if (!bounds) return 'a malformed {n,m} quantifier'; + if (bounds[3] !== undefined && Number(bounds[3]) < Number(bounds[1])) + return 'a {n,m} quantifier with m below n'; + at += bounds[0].length; + } else at += 1; + const next = pattern[at]; + if (next !== undefined && QUANTIFIERS.includes(next)) return 'a quantifier on a quantifier'; + }; + + const disjunction = (depth: number): string | undefined => { + let quantifiable = false; + while (at < pattern.length) { + const char = pattern[at]; + if (char === '|') { + at += 1; + quantifiable = false; + } else if (char === ')') { + if (depth === 0) return 'an unmatched )'; + return; + } else if (char === '^' || char === '$') { + at += 1; + quantifiable = false; + } else if (QUANTIFIERS.includes(char)) { + if (!quantifiable) + return char === '{' ? 'an unescaped {' : 'a quantifier with nothing to repeat'; + const problem = quantifier(); + if (problem) return problem; + quantifiable = false; + } else if (char === '(') { + if (!pattern.startsWith('(?:', at)) return 'a group other than (?:'; + at += 3; + const problem = disjunction(depth + 1); + if (problem) return problem; + if (pattern[at] !== ')') return 'an unclosed group'; + at += 1; + quantifiable = true; + } else if (char === '[') { + const problem = characterClass(); + if (problem) return problem; + quantifiable = true; + } else if (char === '\\') { + const code = escape(false); + if (typeof code === 'string') return code; + quantifiable = true; + } else if (char === '.') return 'an unescaped dot'; + else if (char === ']' || char === '}') return `an unescaped ${char}`; + else { + at += 1; + quantifiable = true; + } + } + }; + + return disjunction(0); +} diff --git a/src/core/pointer.ts b/src/core/pointer.ts deleted file mode 100644 index 989e13b..0000000 --- a/src/core/pointer.ts +++ /dev/null @@ -1,54 +0,0 @@ -// JSON Pointer (RFC 6901). Undefined stands for "nothing there", which no JSON value is. - -const INDEX = /^(?:0|[1-9][0-9]*)$/; - -export const escape = (name: string) => name.replaceAll('~', '~0').replaceAll('/', '~1'); - -export const segments = (pointer: string) => - pointer - .split('/') - .slice(1) - .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~')); - -// Lengths as JSON Schema counts them: in code points, not UTF-16 code units. -export const codePoints = (text: string) => [...text].length; - -/** The pointer, or its nearest ancestor no longer than limit code points. */ -export function within(pointer: string, limit: number) { - const tokens = pointer.split('/'); - while (codePoints(tokens.join('/')) > limit) tokens.pop(); - return tokens.join('/'); -} - -/** The value a pointer names through objects and arrays, or undefined. */ -export function resolve(value: unknown, pointer: string): unknown { - let current = value; - for (const name of segments(pointer)) { - if (Array.isArray(current)) { - if (!INDEX.test(name) || Number(name) >= current.length) return undefined; - current = current[Number(name)]; - } else if (current && typeof current === 'object' && Object.hasOwn(current, name)) - current = (current as Record)[name]; - else return undefined; - } - return current; -} - -/** - * The value a pointer names through object members only, or undefined. The contract - * rules bind form fields only through object properties, so no binding indexes an array. - */ -export function member(value: unknown, pointer: string): unknown { - let current = value; - for (const name of segments(pointer)) { - if ( - !current || - typeof current !== 'object' || - Array.isArray(current) || - !Object.hasOwn(current, name) - ) - return undefined; - current = (current as Record)[name]; - } - return current; -} diff --git a/src/core/schemas.ts b/src/core/schemas.ts deleted file mode 100644 index b4807fc..0000000 --- a/src/core/schemas.ts +++ /dev/null @@ -1,133 +0,0 @@ -// The schema objects within a schema, the form fields block and the references a contract's -// schemas make. -import { CORE_SCHEMA } from './artifacts.ts'; -import { resolve } from './pointer.ts'; -import type { JsonObject } from './types.ts'; - -// The keywords whose value is a schema, a list of schemas, or a map from names to schemas. -// A name in a map is never a keyword, whatever it is called. -const SUBSCHEMA = [ - 'additionalProperties', - 'propertyNames', - 'items', - 'contains', - 'not', - 'if', - 'then', - 'else', - 'unevaluatedItems', - 'unevaluatedProperties', - 'contentSchema', -]; -const LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']; -const MAPS = ['properties', 'patternProperties', '$defs', 'dependentSchemas']; - -/** Every schema object within a schema, the schema itself first. */ -export function* nodes(schema: unknown): Generator { - if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return; - const node = schema as JsonObject; - yield node; - for (const keyword of SUBSCHEMA) yield* nodes(node[keyword]); - for (const keyword of LISTS) - if (Array.isArray(node[keyword])) for (const item of node[keyword]) yield* nodes(item); - for (const keyword of MAPS) { - const map = node[keyword]; - if (map && typeof map === 'object' && !Array.isArray(map)) - for (const item of Object.values(map)) yield* nodes(item); - } -} - -/** Every value a keyword has in the schema objects within a schema. */ -export const values = (schema: unknown, keyword: string) => - [...nodes(schema)].filter((node) => Object.hasOwn(node, keyword)).map((node) => node[keyword]); - -// The core definitions that give a text field's `format` its lexical form. -const FORMATS: Record = { - email: 'address', - uri: 'identifier', - date: 'date', - 'date-time': 'dateTime', -}; - -/** Rules of a form fields block that JSON Schema cannot state. */ -export function formProblems(fields: JsonObject): string[] { - const properties = (fields.properties ?? {}) as Record; - const problems = ((fields.required ?? []) as string[]) - .filter((name) => !Object.hasOwn(properties, name)) - .map((name) => `required field ${name} is not defined`); - for (const [name, field] of Object.entries(properties)) { - const choices = ( - (field.oneOf ?? (field.items as JsonObject | undefined)?.anyOf ?? []) as JsonObject[] - ).map((choice) => choice.const); - if (new Set(choices).size !== choices.length) problems.push(`field ${name} repeats a choice`); - const defaults = field.default === undefined ? [] : [field.default].flat(); - if (choices.length && defaults.some((value) => !choices.includes(value))) - problems.push(`field ${name} defaults to a value it does not offer`); - } - return problems; -} - -/** - * The schema the values of a fields block satisfy: only its fields, each with a text - * field's `format` read as the core lexical form of that name. - */ -export function fieldValuesSchema(fields: JsonObject): JsonObject { - const properties = Object.fromEntries( - Object.entries((fields.properties ?? {}) as Record).map(([name, field]) => { - if (typeof field.format !== 'string') return [name, field]; - const { format, ...rest } = field; - return [name, { ...rest, $ref: `${CORE_SCHEMA}#/$defs/${FORMATS[format as string]}` }]; - }), - ); - return { ...fields, properties, additionalProperties: false }; -} - -// A plain JSON Pointer fragment: nothing percent-encoded, nothing to decode. -const FRAGMENT = /^(?:\/[A-Za-z0-9._~!$&'()*+,;=:@-]*)*$/; -// Keywords that resolve by scope rather than by a pinned address. -const UNPINNED = ['$dynamicRef', '$recursiveRef', '$dynamicAnchor', '$recursiveAnchor', '$anchor']; - -/** - * Why a contract's schemas would resolve anything but a pinned schema, or undefined. No - * keyword resolves by scope, in the contract's schemas or the pinned ones it supplies. In - * the contract's own schemas, no $id or $schema appears but at the request schema's root, - * since it would move references or change the dialect a subschema is read in; no type is - * a list; autocomplete, the form fields block's annotation, is absent; and every - * reference is a string naming a schema in the pinned documents, by URL. - */ -export function referenceProblem( - inline: unknown[], - requestSchema: JsonObject, - pinned: ReadonlyMap, - supplied: JsonObject[], -): string | undefined { - for (const schema of [...inline, requestSchema, ...supplied]) { - const found = UNPINNED.find((keyword) => values(schema, keyword).length); - if (found) return `${found} is not pinned by any digest`; - } - for (const schema of [...inline, requestSchema]) { - const all = [...nodes(schema)]; - const nested = schema === requestSchema ? all.slice(1) : all; - if (nested.some((node) => '$id' in node)) return 'a nested $id would move references'; - if (nested.some((node) => '$schema' in node)) - return 'a nested $schema would change the dialect'; - if (all.some((node) => Array.isArray(node.type))) return 'type names one type, never a list'; - if (all.some((node) => 'autocomplete' in node)) - return 'autocomplete belongs to the form fields block'; - for (const ref of values(schema, '$ref')) { - if (typeof ref !== 'string') return 'a $ref is not a string'; - const [base, fragment = ''] = ref.split(/#(.*)/s); - const document = pinned.get(base); - if (!document) return `${ref} is not a pinned dependency`; - if (fragment && !fragment.startsWith('/')) - return `${ref} names an anchor, not a JSON Pointer`; - if (!FRAGMENT.test(fragment)) return `${ref} is not a plain JSON Pointer`; - const target = resolve(document, fragment); - if ( - typeof target !== 'boolean' && - !(target && typeof target === 'object' && !Array.isArray(target)) - ) - return `${ref} does not name a schema`; - } - } -} diff --git a/src/core/sha256.ts b/src/core/sha256.ts new file mode 100644 index 0000000..bde65a0 --- /dev/null +++ b/src/core/sha256.ts @@ -0,0 +1,63 @@ +// SHA-256 (FIPS 180-4) over bytes, so the core needs no platform crypto and runs the same +// in Node, browsers and workers. + +const K = Uint32Array.from([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]); + +const rotate = (value: number, bits: number) => (value >>> bits) | (value << (32 - bits)); + +/** The lowercase hexadecimal SHA-256 of the bytes, or of the text's UTF-8 encoding. */ +export function sha256(input: string | Uint8Array): string { + const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input; + // The message, a 1 bit, zeros, and its length in bits as a 64-bit big-endian integer. + const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64); + padded.set(bytes); + padded[bytes.length] = 0x80; + const view = new DataView(padded.buffer); + view.setUint32(padded.length - 8, Math.floor(bytes.length / 0x20000000)); + view.setUint32(padded.length - 4, (bytes.length * 8) >>> 0); + + const hash = Uint32Array.from([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]); + const w = new Uint32Array(64); + for (let block = 0; block < padded.length; block += 64) { + for (let t = 0; t < 16; t += 1) w[t] = view.getUint32(block + t * 4); + for (let t = 16; t < 64; t += 1) { + const s0 = rotate(w[t - 15], 7) ^ rotate(w[t - 15], 18) ^ (w[t - 15] >>> 3); + const s1 = rotate(w[t - 2], 17) ^ rotate(w[t - 2], 19) ^ (w[t - 2] >>> 10); + w[t] = w[t - 16] + s0 + w[t - 7] + s1; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let t = 0; t < 64; t += 1) { + const t1 = + h + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[t] + w[t]; + const t2 = (rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + h = g; + g = f; + f = e; + e = (d + t1) >>> 0; + d = c; + c = b; + b = a; + a = (t1 + t2) >>> 0; + } + hash[0] += a; + hash[1] += b; + hash[2] += c; + hash[3] += d; + hash[4] += e; + hash[5] += f; + hash[6] += g; + hash[7] += h; + } + return Array.from(hash, (word) => word.toString(16).padStart(8, '0')).join(''); +} diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 1a4d094..0000000 --- a/src/core/types.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** The documents MAP 0.2 defines and the type contracts that give them meaning. */ - -export type JsonObject = Record; -export type Authority = 'credential' | 'possession'; -export type ResultState = 'accepted' | 'completed' | 'failed' | 'pending' | 'approval-required'; -export type Consequence = - 'refusal' | 'protection' | 'record' | 'disclosure' | 'commitment' | 'authorization' | 'assertion'; - -export interface SchemaReference { - url: string; - canonicalDigest: string; -} - -export interface ContractOperation { - id: string; - effect: string; - authority: Authority[]; - consequences: Consequence[]; - repeatable?: boolean; - fieldBindings?: { input: string; fields: string }[]; - results: { state: ResultState; reasons?: string[]; outputSchema: JsonObject }[]; -} - -export interface TypeContract { - kind: 'MapTypeContract'; - id: string; - version: string; - profile: string; - target: string; - dependencies?: SchemaReference[]; - detailsSchema?: JsonObject; - requestSchema: SchemaReference; - operations: ContractOperation[]; -} - -/** The exact type a description and its requests name. */ -export type TypeReference = { id: string; version: string; contractDigest: string }; -export type Target = { id: string; revision: string; digest: string; title?: string }; - -export type MapDescription = { - '@context': string; - '@type': 'MailAction'; - '@id': string; - profile: string; - type: TypeReference; - describedAt: string; - expiresAt: string; - service: { - id: string; - name: string; - authority: Authority; - resource?: string; - execution: { url: string; resultUrlTemplate: string; resultRetentionSeconds: number }; - onBehalfOf?: { id: string; name: string }; - humanUrl: string; - }; - recipient?: string; - target: Target; - details?: JsonObject; - operations: { id: string; name: string; description: string }[]; -}; - -export type MapRequest = { - kind: 'MapRequest'; - profile: string; - requestId: string; - interactionId: string; - descriptionDigest: string; - type: TypeReference; - operation: string; - input: JsonObject; -}; - -export type MapResult = { - kind: 'MapResult'; - profile: string; - requestId: string; - interactionId: string; - descriptionDigest: string; - type: TypeReference; - operation: string; - state: ResultState; - target: Target; - recordedAt: string; - resultUrl: string; - actor?: string; - approvalUrl?: string; - reason?: string; - output: JsonObject; -}; - -/** An RFC 9457 problem, with MAP's correlation members when it is correlated. */ -export type MapProblem = { - type: string; - title: string; - status: number; - detail: string; - instance?: string; - profile?: string; - requestId?: string; - interactionId?: string; - code?: string; - target?: Target; - errors?: InputError[]; -}; - -/** One way a request's input fails its contract, with a JSON Pointer into the input. */ -export type InputError = { detail: string; pointer: string }; diff --git a/src/forms-0.1.schema.json b/src/forms-0.1.schema.json deleted file mode 100644 index 88f680b..0000000 --- a/src/forms-0.1.schema.json +++ /dev/null @@ -1,329 +0,0 @@ -{ - "$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/implementation.schema.json b/src/implementation.schema.json new file mode 100644 index 0000000..f6d2310 --- /dev/null +++ b/src/implementation.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mailschema.org/artifacts/map-0.3/schemas/implementation.schema.json", + "title": "MAP implementation record", + "type": "object", + "additionalProperties": false, + "required": [ + "service", + "maintainer", + "type", + "operations", + "binding", + "status", + "documentation", + "evidence" + ], + "dependentRequired": { + "artifact": ["format"], + "format": ["artifact"] + }, + "properties": { + "service": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "maintainer": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + } + } + }, + "type": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "version", + "contractDigest" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "version": { + "type": "string", + "minLength": 1 + }, + "contractDigest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$" + } + } + }, + "operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "binding": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Identifier of the binding specification or documented service binding. It is not a request destination. Clients must explicitly support it." + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "url", + "digest", + "digestMode" + ], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$" + }, + "digestMode": { + "enum": [ + "canonical-json", + "bytes" + ], + "description": "canonical-json hashes the RFC 8785 representation; bytes hashes the exact downloaded artifact. Both use SHA-256." + } + } + }, + "format": { + "type": "string", + "minLength": 1 + }, + "status": { + "enum": [ + "Draft", + "Experimental", + "Stable", + "Deprecated", + "Superseded" + ] + }, + "documentation": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "url", + "summary" + ], + "properties": { + "kind": { + "enum": [ + "declaration", + "test-report" + ] + }, + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + } + }, + "successor": { + "type": "string", + "minLength": 1, + "format": "uri", + "pattern": "^https://" + } + } +} diff --git a/src/index.ts b/src/index.ts index cd431d8..ae3dea4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,5 @@ /** - * Mail Action Protocol 0.2 processing and core artifacts, and validation tools for MailSchema - * Registry contributions. + * Mail Action Protocol 0.3 processing: parse and check descriptions, type contracts and + * implementation records, and compute the RFC 8785 digests that identify contracts. */ -import schema from './contribution.schema.json' with { type: 'json' }; - export * from './core/index.js'; - -export { - contributionErrors, - assertContribution, - assertTypeRecord, - referenceErrors, -} from './validation.js'; -export type { CatalogView } from './validation.js'; -export { typeStages } from './model.js'; -export type { Party, TypeDefinition, TypeRecord, Contribution, Implementation } from './model.js'; -export { getMapSchema, getMapContext, getContractFormatSchema, getFormsSchema } from './map.js'; - -/** Return an independent copy of the contribution JSON Schema (Draft 2020-12). */ -export function getContributionSchema(): Record { - return structuredClone(schema); -} - -/** Return the schema for an expanded Registry record, including attribution and history. */ -export function getRecordSchema(): Record { - return { $schema: schema.$schema, $defs: structuredClone(schema.$defs), $ref: '#/$defs/record' }; -} diff --git a/src/map-0.2.jsonld b/src/map-0.2.jsonld deleted file mode 100644 index c97aca3..0000000 --- a/src/map-0.2.jsonld +++ /dev/null @@ -1,91 +0,0 @@ -{ - "@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 deleted file mode 100644 index a87b566..0000000 --- a/src/map-0.2.schema.json +++ /dev/null @@ -1,1033 +0,0 @@ -{ - "$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 deleted file mode 100644 index b267895..0000000 --- a/src/map.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { readFileSync } from 'node:fs'; - -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 artifact('map-0.2.schema.json'); -} - -/** A fresh copy of the MAP 0.2 JSON-LD context. */ -export function getMapContext(): Record { - return artifact('map-0.2.jsonld'); -} - -/** 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'); -} - -/** 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/model.ts b/src/model.ts deleted file mode 100644 index 900de2d..0000000 --- a/src/model.ts +++ /dev/null @@ -1,64 +0,0 @@ -export const typeStages = [ - { status: 'Draft', singular: 'draft', plural: 'drafts' }, - { status: 'Proposal', singular: 'proposal', plural: 'proposals' }, - { status: 'Reuse assessment', singular: 'reuse assessment', plural: 'reuse assessments' }, -] as const; - -export interface Party { - name: string; - url?: string; -} -export interface TypeDefinition { - slug: string; - name: string; - summary: string; - category: string; - status: (typeof typeStages)[number]['status']; - version: string | null; - profile: string; - overview: string; - target: string; - operations: { id: string; name: string; description: string }[]; - inputs: string; - results: string; - permissions: string; - humanRoute: string; - example: { title: string; steps: string[]; exception: string }; - references: { title: string; href: string; description: string }[]; - openQuestions: string[]; - maintainers: Party[]; - definition?: { label: string; href: string }; -} -export interface TypeRecord extends TypeDefinition { - origin: string; - contributors: Party[]; - history: { label: string; description: string; contributionId?: string }[]; -} -interface Submission { - format: 'mailschema-contribution/1'; - id: string; - contributor: Party; - source?: string; - summary: string; -} -export type Contribution = Submission & - ( - | { kind: 'new-type'; record: TypeDefinition } - | { kind: 'amendment'; baseDigest: string; record: TypeDefinition } - | { - kind: 'implementation'; - type: string; - typeVersion: string; - typeDigest: string; - profile: string; - product: Party; - operations: string[]; - evidence: { - kind: 'declaration' | 'test-report'; - url?: string; - reproduction?: string; - results?: string; - }; - } - ); -export type Implementation = Extract; diff --git a/src/profile.json b/src/profile.json new file mode 100644 index 0000000..ff28124 --- /dev/null +++ b/src/profile.json @@ -0,0 +1,27 @@ +{ + "id": "https://mailschema.org/profiles/map/0.3", + "name": "Mail Action Protocol 0.3", + "status": "working-draft", + "description": "An action description carried in Structured Email. A trusted service binding maps each decision to the service's existing operations, and the service enforces permission and exact terms. Types are defined by digest-bound contracts.", + "context": "https://mailschema.org/contexts/map-0.3.jsonld", + "schema": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "contractFormat": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "specification": "https://mailschema.org/specification/core", + "artifacts": { + "context": { + "url": "https://mailschema.org/contexts/map-0.3.jsonld", + "sha256": "a51490d1fe8af695c14ea508aad88fd0429d18564791b6b52f3ab07d5c51aab3" + }, + "schema": { + "url": "https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json", + "sha256": "0ad09a6955a47e7a2c1c1eec0747a3e0b15c08a79860e90ce4e65da11983aeb3" + }, + "contractFormat": { + "url": "https://mailschema.org/artifacts/map-0.3/schemas/contract.schema.json", + "sha256": "86bf057529c4fd2c7f0a880825774afeed31e9f7ba1d99760858a585fe28d18d" + }, + "conformance": { + "url": "https://github.com/mailschema/mailschema/blob/main/conformance/map-0.3/README.md" + } + } +} diff --git a/src/type-contract-0.2.schema.json b/src/type-contract-0.2.schema.json deleted file mode 100644 index 1fe14b2..0000000 --- a/src/type-contract-0.2.schema.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "$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/src/validation.ts b/src/validation.ts deleted file mode 100644 index 64b0f9a..0000000 --- a/src/validation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import Ajv2020 from 'ajv/dist/2020.js'; -import addFormats from 'ajv-formats'; -import schema from './contribution.schema.json' with { type: 'json' }; -import type { Contribution, TypeRecord } from './model.ts'; - -const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false }); -addFormats(ajv); -const contribution = ajv.compile(schema); -const variants = new Map( - schema.oneOf.map((variant) => [ - variant.properties.kind.const, - ajv.compile({ $schema: schema.$schema, $defs: schema.$defs, ...variant }), - ]), -); -const record = ajv.compile({ - $schema: schema.$schema, - $defs: schema.$defs, - $ref: '#/$defs/record', -}); - -export function contributionErrors(value: unknown): string[] { - const kind = value && typeof value === 'object' && 'kind' in value ? value.kind : undefined; - const validate = typeof kind === 'string' ? variants.get(kind) || contribution : contribution; - if (validate(value)) return []; - return (validate.errors || []).map( - (error) => - `${error.instancePath || '/'} ${error.message}${error.keyword === 'required' ? `: ${error.params.missingProperty}` : ''}`, - ); -} - -export function assertContribution(value: unknown): asserts value is Contribution { - const errors = contributionErrors(value); - if (errors.length) throw new Error(`Invalid contribution:\n${errors.join('\n')}`); -} - -export function assertTypeRecord(value: unknown): asserts value is TypeRecord { - if (!record(value)) - throw new Error(`Invalid type record: ${ajv.errorsText(record.errors, { separator: '\n' })}`); -} - -export interface CatalogView { - types: { record: TypeRecord; digest: string }[]; - snapshots: { record: TypeRecord; digest: string }[]; -} -export function referenceErrors(input: Contribution, catalog: CatalogView): string[] { - const errors: string[] = []; - if (input.kind === 'implementation') { - const target = catalog.snapshots.find((item) => item.digest === input.typeDigest)?.record; - if (!target || target.slug !== input.type || target.version !== input.typeVersion) - return [ - 'Unknown type version or record digest. Download the current record from the Registry.', - ]; - if (target.profile !== input.profile) - errors.push('Execution profile does not match the targeted record.'); - const operations = new Set(target.operations.map((operation) => operation.id)); - if ( - new Set(input.operations).size !== input.operations.length || - input.operations.some((operation) => !operations.has(operation)) - ) - errors.push('Unknown or duplicate operation in the implementation declaration.'); - } else { - const existing = catalog.types.find((item) => item.record.slug === input.record.slug); - if (input.kind === 'new-type' && existing) - errors.push('This type already exists. Submit an amendment instead.'); - if (input.kind === 'amendment' && (!existing || existing.digest !== input.baseDigest)) - errors.push( - 'Stale or unknown amendment base. Download the current record and reconcile your changes.', - ); - if ( - catalog.types.some( - (item) => - item.record.slug !== input.record.slug && - item.record.name.toLocaleLowerCase() === input.record.name.toLocaleLowerCase(), - ) - ) - errors.push('Another type already uses this name.'); - const operationIds = input.record.operations.map((operation) => operation.id); - const operationNames = input.record.operations.map((operation) => - operation.name.toLocaleLowerCase(), - ); - if (new Set(operationIds).size !== operationIds.length) - errors.push('Operation identifiers must be unique within a type.'); - if (new Set(operationNames).size !== operationNames.length) - errors.push('Operation names must be unique within a type.'); - } - return errors; -} diff --git a/test/fixtures/contracts/account-activity-0.1.json b/test/fixtures/contracts/account-activity-0.1.json deleted file mode 100644 index d7c0749..0000000 --- a/test/fixtures/contracts/account-activity-0.1.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "Account activity reported by the service, identified in the details.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "summary": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "occurredAt": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - }, - "account": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "eventType": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/httpsIdentifier" - }, - "observedFrom": { - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ip": { - "type": "string", - "pattern": "^[0-9A-Fa-f.:]{2,45}$" - }, - "userAgent": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "location": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - } - } - } - }, - "required": ["summary", "occurredAt", "account"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/account-activity-0.1.schema.json", - "canonicalDigest": "sha-256:e5f65b11cafc864640b5d2210cb4a6a89d02e281ce5e6c7ed820893cb1e7c867" - }, - "operations": [ - { - "id": "confirm", - "effect": "Record that the account holder or their agent performed the activity.", - "authority": ["credential"], - "consequences": ["assertion"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "recognized": { - "const": true - } - }, - "required": ["recognized"] - } - } - ] - }, - { - "id": "report-unrecognized", - "effect": "Report that the activity is not recognized. The service takes its protective steps.", - "authority": ["credential", "possession"], - "consequences": ["protection"], - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "reportRecorded": { - "const": true - } - }, - "required": ["reportRecorded"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/account-security-response-0.1.json b/test/fixtures/contracts/account-security-response-0.1.json new file mode 100644 index 0000000..8a5d7ee --- /dev/null +++ b/test/fixtures/contracts/account-security-response-0.1.json @@ -0,0 +1,113 @@ +{ + "id": "https://mailschema.org/types/account-security-response", + "version": "0.1", + "profile": "https://mailschema.org/profiles/map/0.3", + "name": "Account Security Response", + "summary": "Record that a recipient does not recognize a specific account event, without changing account access.", + "requirements": [ + "The event MUST be a concrete event in the addressed principal's service account. The terms version MUST bind that account, event identifier, occurrence time, activity description, and presented device or location information.", + "The report MUST be recorded as the recipient's unverified protective signal. It MUST NOT claim that fraud occurred or that a device or location was verified merely because the recipient submitted the report.", + "The report MUST NOT by itself lock or delete the account, revoke sessions or credentials, change recovery settings, transfer assets, disclose protected data, or execute another consequential action. Any such response requires a separate authenticated service process and its own authorization.", + "The contract MUST NOT offer a recognize, authorize-login, or approve-device operation. Recognizing an event is not authorization for a new session.", + "For capability use, the event report MUST expire within 259200 seconds of issuedAt and no later than expiresAt. A copied capability can submit only the same report for the same event. It cannot supply a message or identify another event.", + "The email SHOULD minimize device and location information. The human route MUST authenticate the principal before disclosing protected event details beyond the minimal email preview." + ], + "detailsSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": [ + "account", + "event" + ], + "properties": { + "account": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "event": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "occurredAt", + "activity" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "occurredAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + }, + "activity": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "device": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "location": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + } + } + }, + "operations": [ + { + "id": "report-unrecognized", + "name": "Report unrecognized activity", + "semantics": "Record that the recipient reports this event as unrecognized, without executing account remediation or making a verified fraud assertion.", + "effects": [ + "https://mailschema.org/effects/protection", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential", + "capability" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "reported": "The service recorded the protective report; no account remediation is implied." + }, + "capability": { + "kind": "protective-report", + "maxLifetimeSeconds": 259200 + } + } + ] +} diff --git a/test/fixtures/contracts/action-approval-0.1.json b/test/fixtures/contracts/action-approval-0.1.json deleted file mode 100644 index 74216e9..0000000 --- a/test/fixtures/contracts/action-approval-0.1.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "A proposed action at the exact revision the description names. The details state its terms.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "summary": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "requester": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "id": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/httpsIdentifier" - } - }, - "required": ["name"] - }, - "authorizationDetails": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "items": { - "type": "object", - "maxProperties": 50, - "required": ["type"], - "properties": { - "type": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "locations": { - "type": "array", - "items": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/identifier" - }, - "maxItems": 100, - "uniqueItems": true - }, - "actions": { - "type": "array", - "items": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "maxItems": 100, - "uniqueItems": true - }, - "datatypes": { - "type": "array", - "items": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "maxItems": 100, - "uniqueItems": true - }, - "identifier": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "privileges": { - "type": "array", - "items": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "maxItems": 100, - "uniqueItems": true - } - } - } - } - }, - "required": ["summary", "authorizationDetails"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/action-approval-0.1.schema.json", - "canonicalDigest": "sha-256:601dfe4a21dfe2fb50954f7b54ee3b02580595f76b05ad9dafe1d7c6c137cec3" - }, - "operations": [ - { - "id": "approve", - "effect": "Approve the proposal on the terms in its details, or propose approval for a human decision.", - "authority": ["credential"], - "consequences": ["authorization", "commitment", "disclosure"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "approved" - } - }, - "required": ["decision"] - } - }, - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false - } - }, - { - "state": "failed", - "reasons": ["declined", "stale-target", "expired", "superseded", "withdrawn"], - "outputSchema": { - "type": "object", - "additionalProperties": false - } - } - ] - }, - { - "id": "decline", - "effect": "Decline the proposal. The service records the decision and does not run the action.", - "authority": ["credential"], - "consequences": ["refusal"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "declined" - } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/campaign-send-approval-0.1.json b/test/fixtures/contracts/campaign-send-approval-0.1.json new file mode 100644 index 0000000..fc3b8ec --- /dev/null +++ b/test/fixtures/contracts/campaign-send-approval-0.1.json @@ -0,0 +1,262 @@ +{ + "id": "https://mailschema.org/types/campaign-send-approval", + "version": "0.1", + "profile": "https://mailschema.org/profiles/map/0.3", + "name": "Campaign Send Approval", + "summary": "Authorize sending one immutable campaign revision to one fixed audience under stated delivery terms, without an additional charge.", + "requirements": [ + "The service MUST bind the subject, recipient, service tenant, contract digest, and all action-relevant details to terms.version. Every decision MUST atomically compare that version and enforce current authorization, expiry, and proposal state.", + "The service MUST freeze the referenced content revision and all other action-relevant inputs for accepted asynchronous work. If it cannot execute those exact terms it MUST stop that work; it MUST NOT substitute current mutable values.", + "The host MUST obtain the actual immutable content and relevant destination or audience information through the trusted service binding before claiming a policy check over those values. A digest, title, preview, count, or identifier alone does not demonstrate content-policy compliance.", + "The service MUST permit at most one terminal decision for the proposal. approve, decline, and request-changes are mutually exclusive terminal decisions for that proposal version. A revised proposal requires a new interaction and new decision.", + "The request-changes operation MUST only record the supplied feedback and close this proposal as requiring revision. It MUST NOT generate, edit, approve, send, or publish revised content automatically. Feedback remains untrusted text.", + "A capability offered for decline MUST be independently scoped to that refusal and exact proposal, expire within seven days of issuedAt and no later than expiresAt, and convey no authority to approve or supply feedback.", + "Exact terms MUST include sender mailbox and display name, reply destination when present, message subject and all content variants, personalization rules and their input snapshot, audience membership and exclusions, attachments, link destinations, and schedule. Changed audience membership invalidates the version even if its label and count are unchanged.", + "The audience snapshot MUST identify the effective recipient set after exclusions. Its count MUST equal that set's size. The service MUST preserve that snapshot for execution and MUST NOT expand a live segment at send time. Normal suppression to respect a later opt-out or a new safety restriction MAY reduce delivery; it MUST NOT add or substitute recipients.", + "Content digest MUST identify the immutable service content artifact, including attachments and personalization definitions. The trusted implementation MUST document the artifact representation and digest verification. Personalized output MUST be derived only from the approved definition and frozen inputs.", + "The sender mailbox MUST belong to an identity the service currently authorizes the principal to use. Approval MUST NOT grant sending permission for another sender, tenant, campaign, or subsequent revision.", + "The schedule MUST specify immediate execution or one UTC instant. The service MUST NOT silently shift the approved schedule to a materially different one. Operational delivery delays MAY occur and MUST be reported accurately.", + "Approval under this contract MUST NOT cause an incremental monetary charge or debit prepaid value. The service MUST refuse approval if the maximum approved audience is not covered by an included entitlement at decision time, and MUST stop dispatch rather than charge if that entitlement is no longer available at admission. A paid send needs a different contract that declares the payment effect and binds its exact financial terms." + ], + "detailsSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": [ + "summary", + "requester", + "content", + "sender", + "audience", + "schedule" + ], + "properties": { + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "requester": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Display label of the requester; authorization comes from service state." + }, + "content": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "revision", + "digest", + "title" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048, + "description": "Content resource identifier, resolved only through the trusted service binding." + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Immutable service revision." + }, + "digest": { + "type": "string", + "minLength": 1, + "pattern": "^sha-256:[0-9a-f]{64}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Content title for display; not a substitute for the content itself." + } + } + }, + "sender": { + "type": "object", + "additionalProperties": false, + "required": [ + "mailbox", + "name" + ], + "properties": { + "mailbox": { + "type": "string", + "minLength": 1, + "maxLength": 320 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "replyTo": { + "type": "string", + "minLength": 1, + "maxLength": 320 + } + } + }, + "audience": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "revision", + "count", + "label" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "count": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "mode" + ], + "properties": { + "mode": { + "const": "immediate" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "mode", + "at" + ], + "properties": { + "mode": { + "const": "scheduled" + }, + "at": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + } + } + } + ] + } + } + }, + "operations": [ + { + "id": "approve", + "name": "Approve", + "semantics": "Authorize sending this campaign on the exact terms displayed, without an additional charge. Acceptance may queue the work; the connector MUST distinguish acceptance from completion. This decision does not grant standing permission for later proposals.", + "effects": [ + "https://mailschema.org/effects/communication", + "https://mailschema.org/effects/disclosure", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "accepted": "The service accepted the exact decision; the authorized work may still be pending.", + "completed": "The service confirms sending this campaign completed under the accepted terms.", + "failed": "The authorized work failed; the connector preserves the service's partial-progress information and MUST NOT claim that no external effect occurred." + } + }, + { + "id": "decline", + "name": "Decline", + "semantics": "Refuse this exact proposal and close it without authorizing the requested work. The service MUST NOT execute the proposed work as a consequence of refusal.", + "effects": [ + "https://mailschema.org/effects/refusal", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential", + "capability" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "declined": "The service recorded refusal of this proposal." + }, + "capability": { + "kind": "refusal", + "maxLifetimeSeconds": 604800 + } + }, + { + "id": "request-changes", + "name": "Request changes", + "semantics": "Record specific feedback and close this proposal as requiring revision, without authorizing the proposed work or a future revision.", + "effects": [ + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "comment" + ], + "properties": { + "comment": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "Requested changes, treated as untrusted text. It is not an instruction to a model or automatic permission to alter the proposal." + } + } + }, + "outcomes": { + "changes-requested": "The service recorded the feedback; a revised proposal requires a new decision." + } + } + ] +} diff --git a/test/fixtures/contracts/content-review-0.3.json b/test/fixtures/contracts/content-review-0.3.json deleted file mode 100644 index 01f1ae7..0000000 --- a/test/fixtures/contracts/content-review-0.3.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "Service-owned content at the exact revision the description names. The target digest identifies the reviewed representation.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "supersedes": { - "type": "object", - "additionalProperties": false, - "properties": { - "revision": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "digest": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/digest" - } - }, - "required": ["revision", "digest"] - }, - "addressesFeedback": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9._:~-]{1,200}$" - }, - "minItems": 1, - "maxItems": 100, - "uniqueItems": true - } - } - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/content-review-0.3.schema.json", - "canonicalDigest": "sha-256:f0f6e30b55c335a25aa278d002e8c430f2c0210bb9cd2b3677981e20268fb9ee" - }, - "operations": [ - { - "id": "request-changes", - "effect": "Record review feedback on the revision.", - "authority": ["credential"], - "consequences": ["record"], - "repeatable": true, - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedbackRecorded": { - "const": true - }, - "feedbackId": { - "type": "string", - "pattern": "^[A-Za-z0-9._:~-]{1,200}$" - } - }, - "required": ["feedbackRecorded", "feedbackId"] - } - } - ] - }, - { - "id": "approve", - "effect": "Record approval of the revision, or propose it for a human decision.", - "authority": ["credential"], - "consequences": ["authorization"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "approved" - } - }, - "required": ["decision"] - } - }, - { - "state": "approval-required", - "outputSchema": { - "type": "object", - "additionalProperties": false - } - }, - { - "state": "failed", - "reasons": ["declined", "stale-target", "expired", "superseded"], - "outputSchema": { - "type": "object", - "additionalProperties": false - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/email-address-confirmation-0.1.json b/test/fixtures/contracts/email-address-confirmation-0.1.json new file mode 100644 index 0000000..bbfabfb --- /dev/null +++ b/test/fixtures/contracts/email-address-confirmation-0.1.json @@ -0,0 +1,85 @@ +{ + "id": "https://mailschema.org/types/email-address-confirmation", + "version": "0.1", + "profile": "https://mailschema.org/profiles/map/0.3", + "name": "Email Address Confirmation", + "summary": "Confirm access to a mailbox for a principal-initiated communication request, without creating a signed-in session or granting account authority.", + "requirements": [ + "The sole permitted purpose is communication-address: validating a mailbox for future communications requested by its controller. Confirmation MUST NOT itself send a campaign or disclose private data. Subsequent communication requires its own applicable consent and service permission.", + "The service origin, recipient, details.mailbox, details.requestId, and details.purpose MUST match a request recorded independently when the principal initiated it. details.mailbox MUST identify the same controlled mailbox as recipient. The initiator MUST retain enough session or authenticated request state to establish this match; the email cannot create that state.", + "The terms version MUST bind the mailbox, request identifier, purpose, and any associated non-authentication account context. Changes require a new request, interaction, and confirmation.", + "Confirmation MUST NOT authenticate a session, reset a password, link or take over an account, add a recovery address, grant access, or serve as an authentication factor. Those flows require their own established security mechanisms.", + "The interaction and capability MUST expire within 86400 seconds of issuedAt. The service MUST invalidate confirmation authority on first successful use. A duplicate MAY acknowledge prior processing but MUST NOT revalidate or extend confirmation.", + "The human route MUST establish the independently recorded initiation context and require an explicit unsafe-method submission. Fetching the email link MUST NOT confirm the mailbox or consume confirmation authority.", + "The 24-hour and invalidation-on-use limits adopt the design of NIST SP 800-63A-4 section 3.8 for email confirmation codes. This contract does not assert that every email validation use case falls within that publication's identity-proofing scope." + ], + "detailsSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": [ + "mailbox", + "requestId", + "requestedAt", + "purpose", + "serviceName" + ], + "properties": { + "mailbox": { + "type": "string", + "minLength": 1, + "maxLength": 320 + }, + "requestId": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048, + "description": "Identifier recorded in the initiation flow; an email-supplied identifier alone is not correlation evidence." + }, + "requestedAt": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + }, + "purpose": { + "const": "communication-address" + }, + "serviceName": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Display label only. The service origin identifies the destination." + } + } + }, + "operations": [ + { + "id": "confirm", + "name": "Confirm address", + "semantics": "Confirm that the principal controls the addressed mailbox for the independently matched communication request, once only. This is not a login or grant of account access.", + "effects": [ + "https://mailschema.org/effects/assertion", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential", + "capability" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "confirmed": "The service recorded the requested mailbox confirmation." + }, + "capability": { + "kind": "address-confirmation", + "maxLifetimeSeconds": 86400 + } + } + ] +} diff --git a/test/fixtures/contracts/email-confirmation-0.1.json b/test/fixtures/contracts/email-confirmation-0.1.json deleted file mode 100644 index 33e5292..0000000 --- a/test/fixtures/contracts/email-confirmation-0.1.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "A pending request that needs proof of control of an address, identified in the details.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "origin": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/httpsOrigin" - }, - "address": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/address" - }, - "purpose": { - "enum": ["account-creation", "sign-in", "address-change", "subscription"] - }, - "requestedAt": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - } - }, - "required": ["origin", "address", "purpose", "requestedAt"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/email-confirmation-0.1.schema.json", - "canonicalDigest": "sha-256:f7465cc662d83f115eacbf43d936e18daf8515bb18cd44484c11d39864944093" - }, - "operations": [ - { - "id": "confirm", - "effect": "Confirm the pending request, which completes it.", - "authority": ["possession"], - "consequences": ["assertion"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "confirmed": { - "const": true - } - }, - "required": ["confirmed"] - } - } - ] - }, - { - "id": "report-unrecognized", - "effect": "Report that the recipient did not make the request. The service cancels it.", - "authority": ["possession"], - "consequences": ["protection"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "cancelled": { - "const": true - } - }, - "required": ["cancelled"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/event-response-0.1.json b/test/fixtures/contracts/event-response-0.1.json deleted file mode 100644 index 90e5fe3..0000000 --- a/test/fixtures/contracts/event-response-0.1.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "An iCalendar event, or one occurrence of it, identified in the details. The target revision is the invitation SEQUENCE and the target digest is the SHA-256 of the iCalendar part carried in the message.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "event": { - "type": "object", - "additionalProperties": false, - "properties": { - "uid": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 1000 - }, - "recurrenceId": { - "type": "string", - "pattern": "^[0-9]{8}(?:T[0-9]{6}Z)?$" - } - }, - "required": ["uid"] - }, - "attendee": { - "type": "string", - "pattern": "^mailto:[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[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": 330 - } - }, - "required": ["event", "attendee"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/event-response-0.1.schema.json", - "canonicalDigest": "sha-256:79de685cae7a0846d80fb99a7f4b52640475d775174fb127b8093806965d7ca6" - }, - "operations": [ - { - "id": "accept", - "effect": "Record participation status ACCEPTED for the attendee.", - "authority": ["credential", "possession"], - "consequences": ["commitment"], - "repeatable": true, - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "participationStatus": { - "const": "accepted" - } - }, - "required": ["participationStatus"] - } - } - ] - }, - { - "id": "decline", - "effect": "Record participation status DECLINED for the attendee.", - "authority": ["credential", "possession"], - "consequences": ["refusal"], - "repeatable": true, - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "participationStatus": { - "const": "declined" - } - }, - "required": ["participationStatus"] - } - } - ] - }, - { - "id": "respond-tentatively", - "effect": "Record participation status TENTATIVE for the attendee.", - "authority": ["credential", "possession"], - "consequences": ["commitment"], - "repeatable": true, - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "participationStatus": { - "const": "tentative" - } - }, - "required": ["participationStatus"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/information-request-0.1.json b/test/fixtures/contracts/information-request-0.1.json deleted file mode 100644 index 82917d7..0000000 --- a/test/fixtures/contracts/information-request-0.1.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "A request for information at the exact revision the description names. The details define its fields.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - }, - { - "url": "https://mailschema.org/schemas/forms-0.1.schema.json", - "canonicalDigest": "sha-256:5a9ee18da6422bf9641cc1ff7a87a9783a9cb8c1ad34e8bdaadd775e602ebb6a" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "purpose": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - }, - "fields": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/fields" - } - }, - "required": ["purpose", "fields"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/information-request-0.1.schema.json", - "canonicalDigest": "sha-256:75275601985f6afd1b10954929254309d7ba6b6ac490207755cca0dd3bc23522" - }, - "operations": [ - { - "id": "submit-response", - "effect": "Record values for the requested fields.", - "authority": ["credential", "possession"], - "consequences": ["disclosure"], - "fieldBindings": [ - { - "input": "/values", - "fields": "/fields" - } - ], - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "responseRecorded": { - "const": true - } - }, - "required": ["responseRecorded"] - } - } - ] - }, - { - "id": "decline", - "effect": "Record that the recipient will not provide the information.", - "authority": ["credential", "possession"], - "consequences": ["refusal"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "declined" - } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/meeting-scheduling-0.1.json b/test/fixtures/contracts/meeting-scheduling-0.1.json deleted file mode 100644 index 8060206..0000000 --- a/test/fixtures/contracts/meeting-scheduling-0.1.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "An offer of meeting times at the exact revision the description names. The details list the times; whether a time is still free is service state outside the revision.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - }, - { - "url": "https://mailschema.org/schemas/forms-0.1.schema.json", - "canonicalDigest": "sha-256:5a9ee18da6422bf9641cc1ff7a87a9783a9cb8c1ad34e8bdaadd775e602ebb6a" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "summary": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "slots": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "pattern": "^[A-Za-z0-9._-]{1,64}$" - }, - "start": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - }, - "end": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - } - }, - "required": ["id", "start", "end"] - } - }, - "attendeeFields": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/fields" - } - }, - "required": ["summary", "slots"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/meeting-scheduling-0.1.schema.json", - "canonicalDigest": "sha-256:03f03946cb498a452b8a4428a2660e5d01057c8a13ea6039e5302b8d080ba43d" - }, - "operations": [ - { - "id": "book", - "effect": "Book one offered time and create the meeting.", - "authority": ["credential", "possession"], - "consequences": ["commitment", "disclosure"], - "fieldBindings": [ - { - "input": "/values", - "fields": "/attendeeFields" - } - ], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "uid": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 1000 - }, - "start": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - }, - "end": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - } - }, - "required": ["uid", "start", "end"] - } - }, - { - "state": "failed", - "reasons": ["unavailable"], - "outputSchema": { - "type": "object", - "additionalProperties": false - } - } - ] - }, - { - "id": "decline", - "effect": "Record that the recipient will not book any of the offered times.", - "authority": ["credential", "possession"], - "consequences": ["refusal"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "declined" - } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/payment-request-0.1.json b/test/fixtures/contracts/payment-request-0.1.json deleted file mode 100644 index 597cd26..0000000 --- a/test/fixtures/contracts/payment-request-0.1.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "A request to pay at the exact revision the description names. The details state the payee, account, amount and due date; a change to any of them is a new revision.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "creditorName": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "creditorAccount": { - "type": "object", - "additionalProperties": false, - "properties": { - "iban": { - "type": "string", - "pattern": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$" - }, - "bic": { - "type": "string", - "pattern": "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$" - }, - "identification": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - } - }, - "anyOf": [ - { - "required": ["iban"] - }, - { - "required": ["identification"] - } - ] - }, - "creditorIdentifier": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "instructedAmount": { - "type": "object", - "additionalProperties": false, - "properties": { - "currency": { - "type": "string", - "pattern": "^[A-Z]{3}$" - }, - "amount": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]{0,14})(?:\\.[0-9]{1,6})?$" - } - }, - "required": ["currency", "amount"] - }, - "dueDate": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/date" - }, - "remittanceInformationUnstructured": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 140 - }, - "invoice": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 240 - }, - "url": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/httpsIdentifier" - } - }, - "required": ["id"] - } - }, - "required": ["creditorName", "creditorAccount", "instructedAmount", "dueDate"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/payment-request-0.1.schema.json", - "canonicalDigest": "sha-256:6eb8d7811ea81d48fe55242730cb89aac87fb26515bb075400a2d81d5eddbe07" - }, - "operations": [ - { - "id": "accept", - "effect": "Record the payer's commitment to pay on the stated date.", - "authority": ["credential"], - "consequences": ["commitment"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "accepted" - }, - "paymentDate": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/date" - } - }, - "required": ["decision", "paymentDate"] - } - } - ] - }, - { - "id": "decline", - "effect": "Record that the payer refuses the request.", - "authority": ["credential"], - "consequences": ["refusal"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "decision": { - "const": "declined" - } - }, - "required": ["decision"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/publication-approval-0.1.json b/test/fixtures/contracts/publication-approval-0.1.json new file mode 100644 index 0000000..2cb6bba --- /dev/null +++ b/test/fixtures/contracts/publication-approval-0.1.json @@ -0,0 +1,284 @@ +{ + "id": "https://mailschema.org/types/publication-approval", + "version": "0.1", + "profile": "https://mailschema.org/profiles/map/0.3", + "name": "Publication Approval", + "summary": "Authorize publishing one immutable content revision at stated destinations and visibility.", + "requirements": [ + "The service MUST bind the subject, recipient, service tenant, contract digest, and all action-relevant details to terms.version. Every decision MUST atomically compare that version and enforce current authorization, expiry, and proposal state.", + "The service MUST freeze the referenced content revision and all other action-relevant inputs for accepted asynchronous work. If it cannot execute those exact terms it MUST stop that work; it MUST NOT substitute current mutable values.", + "The host MUST obtain the actual immutable content and relevant destination or audience information through the trusted service binding before claiming a policy check over those values. A digest, title, preview, count, or identifier alone does not demonstrate content-policy compliance.", + "The service MUST permit at most one terminal decision for the proposal. approve, decline, and request-changes are mutually exclusive terminal decisions for that proposal version. A revised proposal requires a new interaction and new decision.", + "The request-changes operation MUST only record the supplied feedback and close this proposal as requiring revision. It MUST NOT generate, edit, approve, send, or publish revised content automatically. Feedback remains untrusted text.", + "A capability offered for decline MUST be independently scoped to that refusal and exact proposal, expire within seven days of issuedAt and no later than expiresAt, and convey no authority to approve or supply feedback.", + "Exact terms MUST cover all content and attachments, publication destinations and accounts, visibility, schedule, and the existing revision being replaced, if any. A change to any of these values requires new terms.", + "The destination list MUST enumerate the exact locations affected. A mutable collection label MUST NOT authorize new locations added after approval. For restricted publication, audience.id and audience.revision MUST identify an immutable authorized readership; a vague label such as team is insufficient.", + "Content digest MUST identify the immutable publication artifact. The trusted implementation MUST document its representation and verify the artifact. Approval of a title or an inaccessible digest alone MUST NOT be presented as review of the content.", + "Publication approval MUST NOT change account permissions, purchase promotion, schedule unrelated content, or publish a subsequent revision.", + "This contract authorizes publication. Approval under a different contract MUST NOT be treated as permission to publish without a new, explicit publication decision." + ], + "detailsSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": [ + "summary", + "requester", + "content", + "destinations", + "schedule" + ], + "properties": { + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "requester": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "content": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "revision", + "digest", + "title" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048, + "description": "Content resource identifier, resolved only through the trusted service binding." + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Immutable service revision." + }, + "digest": { + "type": "string", + "minLength": 1, + "pattern": "^sha-256:[0-9a-f]{64}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Content title for display; not a substitute for the content itself." + } + } + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "account", + "visibility" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "account": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "visibility": { + "enum": [ + "public", + "restricted" + ] + }, + "audience": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "revision" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "format": "uri", + "maxLength": 2048 + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + } + }, + "replacesRevision": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "allOf": [ + { + "if": { + "properties": { + "visibility": { + "const": "restricted" + } + } + }, + "then": { + "required": [ + "audience" + ] + }, + "else": { + "not": { + "required": [ + "audience" + ] + } + } + } + ] + }, + "minItems": 1, + "maxItems": 32 + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "mode" + ], + "properties": { + "mode": { + "const": "immediate" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "mode", + "at" + ], + "properties": { + "mode": { + "const": "scheduled" + }, + "at": { + "type": "string", + "minLength": 1, + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + } + } + } + ] + } + } + }, + "operations": [ + { + "id": "approve", + "name": "Approve", + "semantics": "Authorize publishing this content on the exact terms displayed. Acceptance may queue the work; the connector MUST distinguish acceptance from completion. This decision does not grant standing permission for later proposals.", + "effects": [ + "https://mailschema.org/effects/communication", + "https://mailschema.org/effects/disclosure", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "accepted": "The service accepted the exact decision; the authorized work may still be pending.", + "completed": "The service confirms publishing this content completed under the accepted terms.", + "failed": "The authorized work failed; the connector preserves the service's partial-progress information and MUST NOT claim that no external effect occurred." + } + }, + { + "id": "decline", + "name": "Decline", + "semantics": "Refuse this exact proposal and close it without authorizing the requested work. The service MUST NOT execute the proposed work as a consequence of refusal.", + "effects": [ + "https://mailschema.org/effects/refusal", + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential", + "capability" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": null, + "outcomes": { + "declined": "The service recorded refusal of this proposal." + }, + "capability": { + "kind": "refusal", + "maxLifetimeSeconds": 604800 + } + }, + { + "id": "request-changes", + "name": "Request changes", + "semantics": "Record specific feedback and close this proposal as requiring revision, without authorizing the proposed work or a future revision.", + "effects": [ + "https://mailschema.org/effects/state" + ], + "bindings": [ + "credential" + ], + "actors": [ + "human", + "agent" + ], + "exactTerms": true, + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [ + "comment" + ], + "properties": { + "comment": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "Requested changes, treated as untrusted text. It is not an instruction to a model or automatic permission to alter the proposal." + } + } + }, + "outcomes": { + "changes-requested": "The service recorded the feedback; a revised proposal requires a new decision." + } + } + ] +} diff --git a/test/fixtures/contracts/subscription-preferences-0.1.json b/test/fixtures/contracts/subscription-preferences-0.1.json deleted file mode 100644 index a53053f..0000000 --- a/test/fixtures/contracts/subscription-preferences-0.1.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/subscription-preferences", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "A recipient's subscription at the exact revision the description names. The details define the offered settings.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - }, - { - "url": "https://mailschema.org/schemas/forms-0.1.schema.json", - "canonicalDigest": "sha-256:5a9ee18da6422bf9641cc1ff7a87a9783a9cb8c1ad34e8bdaadd775e602ebb6a" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "listId": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "fields": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/choiceFields" - } - }, - "required": ["fields"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/subscription-preferences-0.1.schema.json", - "canonicalDigest": "sha-256:174d6ce3beea3432fba2a75e3e97b00e3a01f5cef99ea2526e89322621de01a2" - }, - "operations": [ - { - "id": "update-preferences", - "effect": "Record the chosen settings.", - "authority": ["credential", "possession"], - "consequences": ["record"], - "repeatable": true, - "fieldBindings": [ - { - "input": "/values", - "fields": "/fields" - } - ], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "effectiveAt": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - } - }, - "required": ["effectiveAt"] - } - } - ] - } - ] -} diff --git a/test/fixtures/contracts/task-assignment-0.1.json b/test/fixtures/contracts/task-assignment-0.1.json deleted file mode 100644 index e62d9f9..0000000 --- a/test/fixtures/contracts/task-assignment-0.1.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "kind": "MapTypeContract", - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "profile": "https://mailschema.org/profiles/map/0.2", - "target": "An assigned to-do identified in the details. The target revision is its SEQUENCE and the target digest is the SHA-256 of the iCalendar part carried in the message.", - "dependencies": [ - { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "canonicalDigest": "sha-256:e7327bd379f75e62e9b6ea387ac80d470e1acf5b3c10060739f43231bfa373d3" - } - ], - "detailsSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "task": { - "type": "object", - "additionalProperties": false, - "properties": { - "uid": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 1000 - } - }, - "required": ["uid"] - }, - "summary": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "due": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/dateTime" - }, - "completionRequirements": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - }, - "required": ["task", "summary"] - }, - "requestSchema": { - "url": "https://mailschema.org/schemas/task-assignment-0.1.schema.json", - "canonicalDigest": "sha-256:ae44cba4881db26a71c850d35952907b4daad0de48e1e612e1a5cfe8b88fcf09" - }, - "operations": [ - { - "id": "accept", - "effect": "Record that the assignee accepts the task.", - "authority": ["credential"], - "consequences": ["commitment"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "participationStatus": { - "const": "accepted" - } - }, - "required": ["participationStatus"] - } - } - ] - }, - { - "id": "decline", - "effect": "Record that the assignee declines the task.", - "authority": ["credential"], - "consequences": ["refusal"], - "results": [ - { - "state": "completed", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "participationStatus": { - "const": "declined" - } - }, - "required": ["participationStatus"] - } - } - ] - }, - { - "id": "report-progress", - "effect": "Record the task's percent complete.", - "authority": ["credential"], - "consequences": ["record"], - "repeatable": true, - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "progressRecorded": { - "const": true - } - }, - "required": ["progressRecorded"] - } - } - ] - }, - { - "id": "report-completion", - "effect": "Record a completion report for the service to verify.", - "authority": ["credential"], - "consequences": ["assertion"], - "repeatable": true, - "results": [ - { - "state": "accepted", - "outputSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "completionRecorded": { - "const": true - } - }, - "required": ["completionRecorded"] - } - } - ] - } - ] -} diff --git a/test/fixtures/examples/account-security-response.json b/test/fixtures/examples/account-security-response.json new file mode 100644 index 0000000..9b3e689 --- /dev/null +++ b/test/fixtures/examples/account-security-response.json @@ -0,0 +1,50 @@ +{ + "@context": "https://mailschema.org/contexts/map-0.3.jsonld", + "@type": "MailAction", + "profile": "https://mailschema.org/profiles/map/0.3", + "type": { + "id": "https://mailschema.org/types/account-security-response", + "version": "0.1", + "contractDigest": "sha-256:ec7c85eb9415914a59be29496f6b50ea77839a579420155f17b39a5968a4e611" + }, + "@id": "urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d43", + "issuedAt": "2026-10-02T08:00:00Z", + "expiresAt": "2026-10-03T08:00:00Z", + "inLanguage": "en", + "service": { + "id": "https://service.example", + "tenant": "urn:example:tenant:7" + }, + "recipient": "owner@example.net", + "subject": { + "id": "urn:example:event:23", + "title": "Review a sign-in from a new device" + }, + "terms": { + "id": "urn:example:security-alert:s23", + "version": "s23-r1" + }, + "human": { + "url": "https://service.example/security/s23" + }, + "details": { + "account": { + "id": "urn:example:account:7", + "label": "Example account" + }, + "event": { + "id": "urn:example:event:23", + "occurredAt": "2026-10-02T07:50:00Z", + "activity": "Sign-in from a new device", + "device": "Example browser" + } + }, + "operations": [ + { + "id": "report-unrecognized", + "capability": { + "url": "https://service.example/c/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + } + ] +} diff --git a/test/fixtures/examples/campaign-send-approval.json b/test/fixtures/examples/campaign-send-approval.json new file mode 100644 index 0000000..9286316 --- /dev/null +++ b/test/fixtures/examples/campaign-send-approval.json @@ -0,0 +1,64 @@ +{ + "@context": "https://mailschema.org/contexts/map-0.3.jsonld", + "@type": "MailAction", + "profile": "https://mailschema.org/profiles/map/0.3", + "type": { + "id": "https://mailschema.org/types/campaign-send-approval", + "version": "0.1", + "contractDigest": "sha-256:de3c88a0712adb1d0c6e82f437b825c83520d8e1c44c7b032a2cfcfe5ce55731" + }, + "@id": "urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d40", + "issuedAt": "2026-10-02T08:00:00Z", + "expiresAt": "2026-10-03T08:00:00Z", + "inLanguage": "en", + "service": { + "id": "https://service.example", + "tenant": "urn:example:tenant:7" + }, + "recipient": "owner@example.net", + "subject": { + "id": "urn:example:campaign:18", + "title": "Send product update" + }, + "terms": { + "id": "urn:example:proposal:c18", + "version": "c18-r2" + }, + "human": { + "url": "https://service.example/review/c18" + }, + "details": { + "summary": "The product update is ready to send to the newsletter audience. Please review the email and approve this send.", + "requester": "Campaign editor", + "content": { + "id": "urn:example:content:18", + "revision": "r2", + "digest": "sha-256:86d8e5d2331a2c3bd211a71adb79149b644104ccbb7a4425b7ac5b26e94ef06d", + "title": "Product update" + }, + "sender": { + "mailbox": "news@service.example", + "name": "Service newsletter" + }, + "audience": { + "id": "urn:example:audience:9", + "revision": "snapshot-3", + "count": 4210, + "label": "Newsletter subscribers" + }, + "schedule": { + "mode": "immediate" + } + }, + "operations": [ + { + "id": "approve" + }, + { + "id": "decline" + }, + { + "id": "request-changes" + } + ] +} diff --git a/test/fixtures/examples/email-address-confirmation.json b/test/fixtures/examples/email-address-confirmation.json new file mode 100644 index 0000000..0b4f577 --- /dev/null +++ b/test/fixtures/examples/email-address-confirmation.json @@ -0,0 +1,45 @@ +{ + "@context": "https://mailschema.org/contexts/map-0.3.jsonld", + "@type": "MailAction", + "profile": "https://mailschema.org/profiles/map/0.3", + "type": { + "id": "https://mailschema.org/types/email-address-confirmation", + "version": "0.1", + "contractDigest": "sha-256:fc9d8a4fb3f3065f93bd53d1f9238b13df63186588d3f3f44982d5813412e282" + }, + "@id": "urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d42", + "issuedAt": "2026-10-02T08:00:00Z", + "expiresAt": "2026-10-03T08:00:00Z", + "inLanguage": "en", + "service": { + "id": "https://service.example", + "tenant": "urn:example:tenant:7" + }, + "recipient": "owner@example.net", + "subject": { + "id": "urn:example:request:17", + "title": "Confirm the requested communication address" + }, + "terms": { + "id": "urn:example:confirmation:e17", + "version": "e17-r1" + }, + "human": { + "url": "https://service.example/confirm/e17" + }, + "details": { + "mailbox": "owner@example.net", + "requestId": "urn:example:request:17", + "requestedAt": "2026-10-02T07:59:00Z", + "purpose": "communication-address", + "serviceName": "Example Service" + }, + "operations": [ + { + "id": "confirm", + "capability": { + "url": "https://service.example/c/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + ] +} diff --git a/test/fixtures/examples/publication-approval.json b/test/fixtures/examples/publication-approval.json new file mode 100644 index 0000000..f8de967 --- /dev/null +++ b/test/fixtures/examples/publication-approval.json @@ -0,0 +1,61 @@ +{ + "@context": "https://mailschema.org/contexts/map-0.3.jsonld", + "@type": "MailAction", + "profile": "https://mailschema.org/profiles/map/0.3", + "type": { + "id": "https://mailschema.org/types/publication-approval", + "version": "0.1", + "contractDigest": "sha-256:de91a49de4cba91209be253456e7c56709791fff877a060560d4eada0cc9468c" + }, + "@id": "urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d41", + "issuedAt": "2026-10-02T08:00:00Z", + "expiresAt": "2026-10-03T08:00:00Z", + "inLanguage": "en", + "service": { + "id": "https://service.example", + "tenant": "urn:example:tenant:7" + }, + "recipient": "owner@example.net", + "subject": { + "id": "urn:example:object:42", + "title": "Publish the getting started guide" + }, + "terms": { + "id": "urn:example:proposal:p7", + "version": "p7-r4" + }, + "human": { + "url": "https://service.example/review/p7" + }, + "details": { + "summary": "Publish the approved getting started guide.", + "requester": "Documentation editor", + "content": { + "id": "urn:example:content:42", + "revision": "r4", + "title": "Getting started", + "digest": "sha-256:4ffdf504c44a73a059c09c9cb30362c04f593b3af405b83bc084bb045067f95c" + }, + "destinations": [ + { + "id": "https://docs.service.example/guide", + "account": "urn:example:account:7", + "visibility": "public" + } + ], + "schedule": { + "mode": "immediate" + } + }, + "operations": [ + { + "id": "approve" + }, + { + "id": "decline" + }, + { + "id": "request-changes" + } + ] +} diff --git a/test/fixtures/jcs-vectors.json b/test/fixtures/jcs-vectors.json new file mode 100644 index 0000000..a550b17 --- /dev/null +++ b/test/fixtures/jcs-vectors.json @@ -0,0 +1,56 @@ +[ + { + "name": "member ordering", + "json": "{\"b\":2,\"a\":1,\"aa\":3,\"A\":4,\"é\":5}", + "canonical": "{\"A\":4,\"a\":1,\"aa\":3,\"b\":2,\"é\":5}", + "digest": "sha-256:ddbc230ff782f59d30f388932a6596b2bbb0510e4bb4883a91f579f0b2afb56b" + }, + { + "name": "member ordering by UTF-16 code units", + "json": "{\"\\u20ac\":\"Euro Sign\",\"\\r\":\"Carriage Return\",\"\\ufb33\":\"Hebrew Letter Dalet With Dagesh\",\"1\":\"One\",\"\\ud83d\\ude00\":\"Emoji: Grinning Face\",\"\\u0080\":\"Control\",\"\\u00f6\":\"Latin Small Letter O With Diaeresis\"}", + "canonical": "{\"\\r\":\"Carriage Return\",\"1\":\"One\",\"€\":\"Control\",\"ö\":\"Latin Small Letter O With Diaeresis\",\"€\":\"Euro Sign\",\"😀\":\"Emoji: Grinning Face\",\"דּ\":\"Hebrew Letter Dalet With Dagesh\"}", + "digest": "sha-256:5e321556d22018a9656991a9e94f77ec175fa193e52a2429d312f8419ec8b08c" + }, + { + "name": "string escapes", + "json": "{\"s\":\"quote\\\" backslash\\\\ tab\\t line\\n nul\\u0000 bell\\u0007\"}", + "canonical": "{\"s\":\"quote\\\" backslash\\\\ tab\\t line\\n nul\\u0000 bell\\u0007\"}", + "digest": "sha-256:a0859d9bd34e9fe424424fee1947f6cececab32b675312ab80afdeff2da421df" + }, + { + "name": "unicode is not normalised", + "json": "{\"s\":\"café ☕ 𝄞 e\\u0301\"}", + "canonical": "{\"s\":\"café ☕ 𝄞 é\"}", + "digest": "sha-256:b2dfcf2c8457081b45e99535ceee25907d48f3df2daf00e4faf5033643017733" + }, + { + "name": "integers", + "json": "{\"n\":[0,-0,1,-1,9007199254740991,-9007199254740991]}", + "canonical": "{\"n\":[0,0,1,-1,9007199254740991,-9007199254740991]}", + "digest": "sha-256:823a8412f1f3318eb3aaa56dd79a435d67e455738a84348efa5124457d90357a" + }, + { + "name": "numbers", + "json": "{\"n\":[1.5,1e21,1e-7,0.1,100,-0.5,1E2]}", + "canonical": "{\"n\":[1.5,1e+21,1e-7,0.1,100,-0.5,100]}", + "digest": "sha-256:36cff5012c572fc6d4f147e26e50a2e21b011eaac8c004594aa168e4002cc2e9" + }, + { + "name": "shortest round-trip numbers", + "json": "{\"n\":[0.30000000000000004,1.2345678901234567,5e-324,1.7976931348623157e308,4.35,0.000001,123e-20,333333333.3333333]}", + "canonical": "{\"n\":[0.30000000000000004,1.2345678901234567,5e-324,1.7976931348623157e+308,4.35,0.000001,1.23e-18,333333333.3333333]}", + "digest": "sha-256:1e74e12ecd69f643e738f639208cac4e9f80ebc7994318786dbcb667e933d42d" + }, + { + "name": "nesting and empties", + "json": "{\"a\":[],\"b\":{},\"c\":[{\"z\":null,\"y\":true,\"x\":false}]}", + "canonical": "{\"a\":[],\"b\":{},\"c\":[{\"x\":false,\"y\":true,\"z\":null}]}", + "digest": "sha-256:50e0ce9f658b6a6ccbd27a7541442b682be84a1a5a2a1c066e50512abebb2797" + }, + { + "name": "Publication Approval description", + "json": "{\"@context\":\"https://mailschema.org/contexts/map-0.3.jsonld\",\"@type\":\"MailAction\",\"profile\":\"https://mailschema.org/profiles/map/0.3\",\"type\":{\"id\":\"https://mailschema.org/types/publication-approval\",\"version\":\"0.1\",\"contractDigest\":\"sha-256:de91a49de4cba91209be253456e7c56709791fff877a060560d4eada0cc9468c\"},\"@id\":\"urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d41\",\"issuedAt\":\"2026-10-02T08:00:00Z\",\"expiresAt\":\"2026-10-03T08:00:00Z\",\"inLanguage\":\"en\",\"service\":{\"id\":\"https://service.example\",\"tenant\":\"urn:example:tenant:7\"},\"recipient\":\"owner@example.net\",\"subject\":{\"id\":\"urn:example:object:42\",\"title\":\"Publish the getting started guide\"},\"terms\":{\"id\":\"urn:example:proposal:p7\",\"version\":\"p7-r4\"},\"human\":{\"url\":\"https://service.example/review/p7\"},\"details\":{\"summary\":\"Publish the approved getting started guide.\",\"requester\":\"Documentation editor\",\"content\":{\"id\":\"urn:example:content:42\",\"revision\":\"r4\",\"title\":\"Getting started\",\"digest\":\"sha-256:4ffdf504c44a73a059c09c9cb30362c04f593b3af405b83bc084bb045067f95c\"},\"destinations\":[{\"id\":\"https://docs.service.example/guide\",\"account\":\"urn:example:account:7\",\"visibility\":\"public\"}],\"schedule\":{\"mode\":\"immediate\"}},\"operations\":[{\"id\":\"approve\"},{\"id\":\"decline\"},{\"id\":\"request-changes\"}]}", + "canonical": "{\"@context\":\"https://mailschema.org/contexts/map-0.3.jsonld\",\"@id\":\"urn:uuid:0199a6c2-4b1e-7c3a-9d2e-5f8a1b2c3d41\",\"@type\":\"MailAction\",\"details\":{\"content\":{\"digest\":\"sha-256:4ffdf504c44a73a059c09c9cb30362c04f593b3af405b83bc084bb045067f95c\",\"id\":\"urn:example:content:42\",\"revision\":\"r4\",\"title\":\"Getting started\"},\"destinations\":[{\"account\":\"urn:example:account:7\",\"id\":\"https://docs.service.example/guide\",\"visibility\":\"public\"}],\"requester\":\"Documentation editor\",\"schedule\":{\"mode\":\"immediate\"},\"summary\":\"Publish the approved getting started guide.\"},\"expiresAt\":\"2026-10-03T08:00:00Z\",\"human\":{\"url\":\"https://service.example/review/p7\"},\"inLanguage\":\"en\",\"issuedAt\":\"2026-10-02T08:00:00Z\",\"operations\":[{\"id\":\"approve\"},{\"id\":\"decline\"},{\"id\":\"request-changes\"}],\"profile\":\"https://mailschema.org/profiles/map/0.3\",\"recipient\":\"owner@example.net\",\"service\":{\"id\":\"https://service.example\",\"tenant\":\"urn:example:tenant:7\"},\"subject\":{\"id\":\"urn:example:object:42\",\"title\":\"Publish the getting started guide\"},\"terms\":{\"id\":\"urn:example:proposal:p7\",\"version\":\"p7-r4\"},\"type\":{\"contractDigest\":\"sha-256:de91a49de4cba91209be253456e7c56709791fff877a060560d4eada0cc9468c\",\"id\":\"https://mailschema.org/types/publication-approval\",\"version\":\"0.1\"}}", + "digest": "sha-256:270b6c781f876aa56ecdc7b9ab45ed5dc0e8465bc8df56f1805e05a8f3a6db44" + } +] diff --git a/test/fixtures/map-0.2/ijson-vectors.json b/test/fixtures/json-vectors.json similarity index 99% rename from test/fixtures/map-0.2/ijson-vectors.json rename to test/fixtures/json-vectors.json index 82140aa..322971d 100644 --- a/test/fixtures/map-0.2/ijson-vectors.json +++ b/test/fixtures/json-vectors.json @@ -276,8 +276,7 @@ { "name": "a number that underflows to zero", "json": "{\"a\":1e-400}", - "valid": true, - "canonical": "{\"a\":0}" + "valid": false }, { "name": "the smallest subnormal", @@ -294,8 +293,7 @@ { "name": "a subnormal just below halfway", "json": "{\"a\":2.4703282292062327e-324}", - "valid": true, - "canonical": "{\"a\":0}" + "valid": false }, { "name": "the shortest round trip", @@ -317,5 +315,22 @@ "name": "an overlong encoding", "base64": "eyJhIjoiwK8ifQ==", "valid": false + }, + { + "name": "exact zero written with a large exponent", + "json": "{\"a\":0e-400}", + "valid": true, + "canonical": "{\"a\":0}" + }, + { + "name": "a number token of 64 characters", + "json": "{\"a\":1.00000000000000000000000000000000000000000000000000000000000000}", + "valid": true, + "canonical": "{\"a\":1}" + }, + { + "name": "a number token of 65 characters", + "json": "{\"a\":1.000000000000000000000000000000000000000000000000000000000000000}", + "valid": false } ] diff --git a/test/fixtures/map-0.2/account-activity/confirm.request.json b/test/fixtures/map-0.2/account-activity/confirm.request.json deleted file mode 100644 index b844fa6..0000000 --- a/test/fixtures/map-0.2/account-activity/confirm.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001901", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000010", - "descriptionDigest": "sha-256:3407464776024d53292e3d0dfce746f6198f7c92d6c5ddc3029d27510f8bf500", - "type": { - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "contractDigest": "sha-256:91270db59b40b9d3b0ffb5b0a0c863ebf30b7ecc4d3f784297279ea1c2a83491" - }, - "operation": "confirm", - "input": {} -} diff --git a/test/fixtures/map-0.2/account-activity/confirm.result.json b/test/fixtures/map-0.2/account-activity/confirm.result.json deleted file mode 100644 index 87955b4..0000000 --- a/test/fixtures/map-0.2/account-activity/confirm.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001901", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000010", - "descriptionDigest": "sha-256:3407464776024d53292e3d0dfce746f6198f7c92d6c5ddc3029d27510f8bf500", - "type": { - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "contractDigest": "sha-256:91270db59b40b9d3b0ffb5b0a0c863ebf30b7ecc4d3f784297279ea1c2a83491" - }, - "operation": "confirm", - "state": "completed", - "target": { - "id": "https://id.example/accounts/7781/activity/9921", - "revision": "1", - "title": "New sign-in from Chrome on Windows", - "digest": "sha-256:d3cccd9ebca114c26cff35880daf8017bae27dfa1ef8a6288cc0929355d02c64" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://id.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001901", - "actor": "agent-client-3", - "output": { - "recognized": true - } -} diff --git a/test/fixtures/map-0.2/account-activity/description.json b/test/fixtures/map-0.2/account-activity/description.json deleted file mode 100644 index 53ee143..0000000 --- a/test/fixtures/map-0.2/account-activity/description.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000010", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "contractDigest": "sha-256:91270db59b40b9d3b0ffb5b0a0c863ebf30b7ecc4d3f784297279ea1c2a83491" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://id.example/service", - "name": "Example Identity", - "authority": "credential", - "resource": "https://id.example/", - "execution": { - "url": "https://id.example/map/actions", - "resultUrlTemplate": "https://id.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://id.example/security/activity/9921" - }, - "target": { - "id": "https://id.example/accounts/7781/activity/9921", - "revision": "1", - "title": "New sign-in from Chrome on Windows", - "digest": "sha-256:d3cccd9ebca114c26cff35880daf8017bae27dfa1ef8a6288cc0929355d02c64" - }, - "details": { - "summary": "A new sign-in from Chrome on Windows.", - "occurredAt": "2026-09-25T07:41:00Z", - "account": "a•••@example.org", - "eventType": "https://schemas.openid.net/secevent/caep/event-type/session-established", - "observedFrom": { - "ip": "203.0.113.24", - "userAgent": "Chrome 141 on Windows", - "location": "Lisbon, Portugal" - } - }, - "operations": [ - { - "id": "confirm", - "name": "Confirm", - "description": "Confirm that you signed in." - }, - { - "id": "report-unrecognized", - "name": "Report unrecognized", - "description": "Report that the sign-in was not you." - } - ] -} diff --git a/test/fixtures/map-0.2/account-activity/report-unrecognized.request.json b/test/fixtures/map-0.2/account-activity/report-unrecognized.request.json deleted file mode 100644 index 0c22461..0000000 --- a/test/fixtures/map-0.2/account-activity/report-unrecognized.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001911", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000010", - "descriptionDigest": "sha-256:3407464776024d53292e3d0dfce746f6198f7c92d6c5ddc3029d27510f8bf500", - "type": { - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "contractDigest": "sha-256:91270db59b40b9d3b0ffb5b0a0c863ebf30b7ecc4d3f784297279ea1c2a83491" - }, - "operation": "report-unrecognized", - "input": { - "note": "Not me; I was asleep." - } -} diff --git a/test/fixtures/map-0.2/account-activity/report-unrecognized.result.json b/test/fixtures/map-0.2/account-activity/report-unrecognized.result.json deleted file mode 100644 index 1d0d7f0..0000000 --- a/test/fixtures/map-0.2/account-activity/report-unrecognized.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001911", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000010", - "descriptionDigest": "sha-256:3407464776024d53292e3d0dfce746f6198f7c92d6c5ddc3029d27510f8bf500", - "type": { - "id": "https://mailschema.org/types/account-activity", - "version": "0.1", - "contractDigest": "sha-256:91270db59b40b9d3b0ffb5b0a0c863ebf30b7ecc4d3f784297279ea1c2a83491" - }, - "operation": "report-unrecognized", - "state": "accepted", - "target": { - "id": "https://id.example/accounts/7781/activity/9921", - "revision": "1", - "title": "New sign-in from Chrome on Windows", - "digest": "sha-256:d3cccd9ebca114c26cff35880daf8017bae27dfa1ef8a6288cc0929355d02c64" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://id.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001911", - "actor": "agent-client-3", - "output": { - "reportRecorded": true - } -} diff --git a/test/fixtures/map-0.2/action-approval/approve.approval-required.json b/test/fixtures/map-0.2/action-approval/approve.approval-required.json deleted file mode 100644 index b4f3104..0000000 --- a/test/fixtures/map-0.2/action-approval/approve.approval-required.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001101", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "approve", - "state": "approval-required", - "target": { - "id": "https://buy.example/proposals/po-881", - "revision": "2", - "title": "Design tool licence, 20 seats", - "digest": "sha-256:242267203e45d3ace6e2e0e2d81a2cdb77e77710f42151be3c6ec33a453ce244" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://buy.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001101", - "actor": "agent-client-3", - "approvalUrl": "https://buy.example/approvals/urn%3Auuid%3A01926a00-0000-7000-8000-000000001101", - "output": {} -} diff --git a/test/fixtures/map-0.2/action-approval/approve.declined.json b/test/fixtures/map-0.2/action-approval/approve.declined.json deleted file mode 100644 index dbb61d4..0000000 --- a/test/fixtures/map-0.2/action-approval/approve.declined.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001101", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "approve", - "state": "failed", - "target": { - "id": "https://buy.example/proposals/po-881", - "revision": "2", - "title": "Design tool licence, 20 seats", - "digest": "sha-256:242267203e45d3ace6e2e0e2d81a2cdb77e77710f42151be3c6ec33a453ce244" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://buy.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001101", - "actor": "agent-client-3", - "reason": "declined", - "output": {} -} diff --git a/test/fixtures/map-0.2/action-approval/approve.request.json b/test/fixtures/map-0.2/action-approval/approve.request.json deleted file mode 100644 index af97ab7..0000000 --- a/test/fixtures/map-0.2/action-approval/approve.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001101", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "approve", - "input": {} -} diff --git a/test/fixtures/map-0.2/action-approval/approve.result.json b/test/fixtures/map-0.2/action-approval/approve.result.json deleted file mode 100644 index acdb15a..0000000 --- a/test/fixtures/map-0.2/action-approval/approve.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001101", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "approve", - "state": "completed", - "target": { - "id": "https://buy.example/proposals/po-881", - "revision": "2", - "title": "Design tool licence, 20 seats", - "digest": "sha-256:242267203e45d3ace6e2e0e2d81a2cdb77e77710f42151be3c6ec33a453ce244" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://buy.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001101", - "actor": "agent-client-3", - "output": { - "decision": "approved" - } -} diff --git a/test/fixtures/map-0.2/action-approval/decline.request.json b/test/fixtures/map-0.2/action-approval/decline.request.json deleted file mode 100644 index 3b66aee..0000000 --- a/test/fixtures/map-0.2/action-approval/decline.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001111", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "decline", - "input": { - "reason": "Use the existing enterprise licence instead." - } -} diff --git a/test/fixtures/map-0.2/action-approval/decline.result.json b/test/fixtures/map-0.2/action-approval/decline.result.json deleted file mode 100644 index 4da4055..0000000 --- a/test/fixtures/map-0.2/action-approval/decline.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001111", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "descriptionDigest": "sha-256:d28578beda1b3fb0706ceb258fb79b815d430a760ddd2fa5966734dd5ed1f241", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://buy.example/proposals/po-881", - "revision": "2", - "title": "Design tool licence, 20 seats", - "digest": "sha-256:242267203e45d3ace6e2e0e2d81a2cdb77e77710f42151be3c6ec33a453ce244" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://buy.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001111", - "actor": "agent-client-3", - "output": { - "decision": "declined" - } -} diff --git a/test/fixtures/map-0.2/action-approval/description.json b/test/fixtures/map-0.2/action-approval/description.json deleted file mode 100644 index c823053..0000000 --- a/test/fixtures/map-0.2/action-approval/description.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000002", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/action-approval", - "version": "0.1", - "contractDigest": "sha-256:c7829a31570c74cebcb87cdaf6627641b0fb23cb77446cce825d68c94edf4c31" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://buy.example/service", - "name": "Example Purchasing", - "authority": "credential", - "resource": "https://buy.example/", - "execution": { - "url": "https://buy.example/map/actions", - "resultUrlTemplate": "https://buy.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://buy.example/approvals/po-881" - }, - "target": { - "id": "https://buy.example/proposals/po-881", - "revision": "2", - "title": "Design tool licence, 20 seats", - "digest": "sha-256:242267203e45d3ace6e2e0e2d81a2cdb77e77710f42151be3c6ec33a453ce244" - }, - "details": { - "summary": "A 12-month licence for 20 seats of a design tool, from Figment Design Ltd.", - "requester": { - "name": "Priya Shah" - }, - "authorizationDetails": [ - { - "type": "https://buy.example/terms/purchase", - "actions": [ - "purchase" - ], - "identifier": "po-881", - "supplier": "Figment Design Ltd", - "seats": 20, - "termMonths": 12, - "total": { - "currency": "EUR", - "amount": "4800.00" - } - } - ] - }, - "operations": [ - { - "id": "approve", - "name": "Approve", - "description": "Approve purchase order 881 as stated." - }, - { - "id": "decline", - "name": "Decline", - "description": "Decline purchase order 881." - } - ] -} diff --git a/test/fixtures/map-0.2/content-review/approve.approval-required.json b/test/fixtures/map-0.2/content-review/approve.approval-required.json deleted file mode 100644 index fd9862b..0000000 --- a/test/fixtures/map-0.2/content-review/approve.approval-required.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001011", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "approve", - "state": "approval-required", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:0ee3ba1e0357f0d96da4f7f04b2d6ef5c8e3cab055f3ffd97204e530eef6e143" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001011", - "actor": "agent-client-3", - "approvalUrl": "https://reviews.example/approvals/urn%3Auuid%3A01926a00-0000-7000-8000-000000001011", - "output": {} -} diff --git a/test/fixtures/map-0.2/content-review/approve.declined.json b/test/fixtures/map-0.2/content-review/approve.declined.json deleted file mode 100644 index d55a103..0000000 --- a/test/fixtures/map-0.2/content-review/approve.declined.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001011", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "approve", - "state": "failed", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:0ee3ba1e0357f0d96da4f7f04b2d6ef5c8e3cab055f3ffd97204e530eef6e143" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001011", - "actor": "agent-client-3", - "reason": "declined", - "output": {} -} diff --git a/test/fixtures/map-0.2/content-review/approve.request.json b/test/fixtures/map-0.2/content-review/approve.request.json deleted file mode 100644 index 0bd9e77..0000000 --- a/test/fixtures/map-0.2/content-review/approve.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001011", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "approve", - "input": {} -} diff --git a/test/fixtures/map-0.2/content-review/approve.result.json b/test/fixtures/map-0.2/content-review/approve.result.json deleted file mode 100644 index 6f14c76..0000000 --- a/test/fixtures/map-0.2/content-review/approve.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001011", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "approve", - "state": "completed", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:0ee3ba1e0357f0d96da4f7f04b2d6ef5c8e3cab055f3ffd97204e530eef6e143" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001011", - "actor": "agent-client-3", - "output": { - "decision": "approved" - } -} diff --git a/test/fixtures/map-0.2/content-review/description.json b/test/fixtures/map-0.2/content-review/description.json deleted file mode 100644 index dc246b9..0000000 --- a/test/fixtures/map-0.2/content-review/description.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://reviews.example/service", - "name": "Example Review Service", - "authority": "credential", - "resource": "https://reviews.example/", - "execution": { - "url": "https://reviews.example/map/actions", - "resultUrlTemplate": "https://reviews.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://reviews.example/reviews/campaign-42/revision/4" - }, - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:0ee3ba1e0357f0d96da4f7f04b2d6ef5c8e3cab055f3ffd97204e530eef6e143" - }, - "details": { - "supersedes": { - "revision": "3", - "digest": "sha-256:752ea73add0d882bfab951804ddfa745bb09e23216b77413e82e8db75432b0bf" - }, - "addressesFeedback": [ - "feedback-7c1d2e3f4a5b6c7d" - ] - }, - "operations": [ - { - "id": "request-changes", - "name": "Request changes", - "description": "Record feedback on revision 4." - }, - { - "id": "approve", - "name": "Approve", - "description": "Record approval of revision 4." - } - ] -} diff --git a/test/fixtures/map-0.2/content-review/request-changes.request.json b/test/fixtures/map-0.2/content-review/request-changes.request.json deleted file mode 100644 index a110d71..0000000 --- a/test/fixtures/map-0.2/content-review/request-changes.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001001", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "request-changes", - "input": { - "feedback": "Add the event timezone to the opening paragraph." - } -} diff --git a/test/fixtures/map-0.2/content-review/request-changes.result.json b/test/fixtures/map-0.2/content-review/request-changes.result.json deleted file mode 100644 index e09c2cb..0000000 --- a/test/fixtures/map-0.2/content-review/request-changes.result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001001", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "descriptionDigest": "sha-256:34e7725e0ebedcccae4403742ca84accfea3b2d9a4092b4e9bdfb91c85cc7a6c", - "type": { - "id": "https://mailschema.org/types/content-review", - "version": "0.3", - "contractDigest": "sha-256:4d5eef369cd6fbeb219221ae3d1c1dd3eeb8e751fd1b634f1e131838558519b7" - }, - "operation": "request-changes", - "state": "accepted", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "4", - "title": "September product update", - "digest": "sha-256:0ee3ba1e0357f0d96da4f7f04b2d6ef5c8e3cab055f3ffd97204e530eef6e143" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://reviews.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001001", - "actor": "agent-client-3", - "output": { - "feedbackRecorded": true, - "feedbackId": "feedback-89984d993dddee07" - } -} diff --git a/test/fixtures/map-0.2/content-review/stale-target.problem.json b/test/fixtures/map-0.2/content-review/stale-target.problem.json deleted file mode 100644 index 4010b3a..0000000 --- a/test/fixtures/map-0.2/content-review/stale-target.problem.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "https://mailschema.org/problems/stale-target", - "title": "The target revision is stale", - "status": 409, - "detail": "The target has changed since this interaction was described. No effect was applied.", - "instance": "https://reviews.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001011", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001011", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "code": "stale-target", - "target": { - "id": "https://reviews.example/content/campaign-42", - "revision": "5", - "title": "September product update", - "digest": "sha-256:3faacaa87a2a9e8ca6236e286a19af95a13ce5c25d6252db2d36c7c35786430e" - } -} diff --git a/test/fixtures/map-0.2/email-confirmation/confirm.request.json b/test/fixtures/map-0.2/email-confirmation/confirm.request.json deleted file mode 100644 index dfcbffc..0000000 --- a/test/fixtures/map-0.2/email-confirmation/confirm.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001801", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000009", - "descriptionDigest": "sha-256:a38fcb9a3a5f4389956d4563f62ff29bc2a8bcd25c70b3f651e311ab47814bf0", - "type": { - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "contractDigest": "sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3" - }, - "operation": "confirm", - "input": {} -} diff --git a/test/fixtures/map-0.2/email-confirmation/confirm.result.json b/test/fixtures/map-0.2/email-confirmation/confirm.result.json deleted file mode 100644 index d64bc11..0000000 --- a/test/fixtures/map-0.2/email-confirmation/confirm.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001801", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000009", - "descriptionDigest": "sha-256:a38fcb9a3a5f4389956d4563f62ff29bc2a8bcd25c70b3f651e311ab47814bf0", - "type": { - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "contractDigest": "sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3" - }, - "operation": "confirm", - "state": "completed", - "target": { - "id": "https://accounts.example.com/pending/signup-5520", - "revision": "1", - "title": "Confirm your new account", - "digest": "sha-256:8fcc87f86199cbd5c4316d65bdca185cf3a232ab1ed43454ec0dfacaa2cfd216" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001801", - "output": { - "confirmed": true - } -} diff --git a/test/fixtures/map-0.2/email-confirmation/description.json b/test/fixtures/map-0.2/email-confirmation/description.json deleted file mode 100644 index 474e301..0000000 --- a/test/fixtures/map-0.2/email-confirmation/description.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000009", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "contractDigest": "sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://accounts.example.com/service", - "name": "Example Developer Tools", - "authority": "possession", - "execution": { - "url": "https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG", - "resultUrlTemplate": "https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://accounts.example.com/signup/pending" - }, - "recipient": "alex+devtools-7f3a@example.org", - "target": { - "id": "https://accounts.example.com/pending/signup-5520", - "revision": "1", - "title": "Confirm your new account", - "digest": "sha-256:8fcc87f86199cbd5c4316d65bdca185cf3a232ab1ed43454ec0dfacaa2cfd216" - }, - "details": { - "origin": "https://devtools.example.com", - "address": "alex+devtools-7f3a@example.org", - "purpose": "account-creation", - "requestedAt": "2026-09-25T07:58:00Z" - }, - "operations": [ - { - "id": "confirm", - "name": "Confirm", - "description": "Confirm this address and create the account." - }, - { - "id": "report-unrecognized", - "name": "Report unrecognized", - "description": "Report that you did not sign up." - } - ] -} diff --git a/test/fixtures/map-0.2/email-confirmation/report-unrecognized.request.json b/test/fixtures/map-0.2/email-confirmation/report-unrecognized.request.json deleted file mode 100644 index 583df1e..0000000 --- a/test/fixtures/map-0.2/email-confirmation/report-unrecognized.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001811", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000009", - "descriptionDigest": "sha-256:a38fcb9a3a5f4389956d4563f62ff29bc2a8bcd25c70b3f651e311ab47814bf0", - "type": { - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "contractDigest": "sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3" - }, - "operation": "report-unrecognized", - "input": {} -} diff --git a/test/fixtures/map-0.2/email-confirmation/report-unrecognized.result.json b/test/fixtures/map-0.2/email-confirmation/report-unrecognized.result.json deleted file mode 100644 index f24eadd..0000000 --- a/test/fixtures/map-0.2/email-confirmation/report-unrecognized.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001811", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000009", - "descriptionDigest": "sha-256:a38fcb9a3a5f4389956d4563f62ff29bc2a8bcd25c70b3f651e311ab47814bf0", - "type": { - "id": "https://mailschema.org/types/email-confirmation", - "version": "0.1", - "contractDigest": "sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3" - }, - "operation": "report-unrecognized", - "state": "completed", - "target": { - "id": "https://accounts.example.com/pending/signup-5520", - "revision": "1", - "title": "Confirm your new account", - "digest": "sha-256:8fcc87f86199cbd5c4316d65bdca185cf3a232ab1ed43454ec0dfacaa2cfd216" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001811", - "output": { - "cancelled": true - } -} diff --git a/test/fixtures/map-0.2/event-response/accept.request.json b/test/fixtures/map-0.2/event-response/accept.request.json deleted file mode 100644 index ebc6bd6..0000000 --- a/test/fixtures/map-0.2/event-response/accept.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001301", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "accept", - "input": {} -} diff --git a/test/fixtures/map-0.2/event-response/accept.result.json b/test/fixtures/map-0.2/event-response/accept.result.json deleted file mode 100644 index bf1007c..0000000 --- a/test/fixtures/map-0.2/event-response/accept.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001301", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "accept", - "state": "completed", - "target": { - "id": "https://calendar.example.net/events/workshop-2026-10", - "revision": "2", - "title": "Design systems workshop", - "digest": "sha-256:8f0e5f23721afc145bd03629c9353659080ffbc397119aeae6d52c19be471616" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://calendar.example.net/map/c/Wf2kR8xLq5Nv1Tz7Mb4Hc9Ds3Pj6Ya0G/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001301", - "output": { - "participationStatus": "accepted" - } -} diff --git a/test/fixtures/map-0.2/event-response/decline.request.json b/test/fixtures/map-0.2/event-response/decline.request.json deleted file mode 100644 index 38b877a..0000000 --- a/test/fixtures/map-0.2/event-response/decline.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001311", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "decline", - "input": { - "comment": "Travelling that week." - } -} diff --git a/test/fixtures/map-0.2/event-response/decline.result.json b/test/fixtures/map-0.2/event-response/decline.result.json deleted file mode 100644 index 05884de..0000000 --- a/test/fixtures/map-0.2/event-response/decline.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001311", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://calendar.example.net/events/workshop-2026-10", - "revision": "2", - "title": "Design systems workshop", - "digest": "sha-256:8f0e5f23721afc145bd03629c9353659080ffbc397119aeae6d52c19be471616" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://calendar.example.net/map/c/Wf2kR8xLq5Nv1Tz7Mb4Hc9Ds3Pj6Ya0G/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001311", - "output": { - "participationStatus": "declined" - } -} diff --git a/test/fixtures/map-0.2/event-response/description.json b/test/fixtures/map-0.2/event-response/description.json deleted file mode 100644 index d0389d2..0000000 --- a/test/fixtures/map-0.2/event-response/description.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://calendar.example.net/service", - "name": "Example Calendar", - "authority": "possession", - "execution": { - "url": "https://calendar.example.net/map/c/Wf2kR8xLq5Nv1Tz7Mb4Hc9Ds3Pj6Ya0G", - "resultUrlTemplate": "https://calendar.example.net/map/c/Wf2kR8xLq5Nv1Tz7Mb4Hc9Ds3Pj6Ya0G/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://calendar.example.net/events/workshop-2026-10" - }, - "recipient": "alex@example.org", - "target": { - "id": "https://calendar.example.net/events/workshop-2026-10", - "revision": "2", - "title": "Design systems workshop", - "digest": "sha-256:8f0e5f23721afc145bd03629c9353659080ffbc397119aeae6d52c19be471616" - }, - "details": { - "event": { - "uid": "workshop-2026-10@calendar.example.net" - }, - "attendee": "mailto:alex@example.org" - }, - "operations": [ - { - "id": "accept", - "name": "Accept", - "description": "Attend the workshop." - }, - { - "id": "decline", - "name": "Decline", - "description": "Do not attend the workshop." - }, - { - "id": "respond-tentatively", - "name": "Respond tentatively", - "description": "Tentatively attend the workshop." - } - ] -} diff --git a/test/fixtures/map-0.2/event-response/invitation.ics b/test/fixtures/map-0.2/event-response/invitation.ics deleted file mode 100644 index 5787fd9..0000000 --- a/test/fixtures/map-0.2/event-response/invitation.ics +++ /dev/null @@ -1,15 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Example Calendar//MAP fixture//EN -METHOD:REQUEST -BEGIN:VEVENT -UID:workshop-2026-10@calendar.example.net -SEQUENCE:2 -DTSTAMP:20260925T080000Z -DTSTART:20261014T090000Z -DTEND:20261014T120000Z -SUMMARY:Design systems workshop -ORGANIZER:mailto:events@calendar.example.net -ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:alex@example.org -END:VEVENT -END:VCALENDAR diff --git a/test/fixtures/map-0.2/event-response/respond-tentatively.request.json b/test/fixtures/map-0.2/event-response/respond-tentatively.request.json deleted file mode 100644 index bbab104..0000000 --- a/test/fixtures/map-0.2/event-response/respond-tentatively.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001321", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "respond-tentatively", - "input": { - "comment": "Likely, pending travel." - } -} diff --git a/test/fixtures/map-0.2/event-response/respond-tentatively.result.json b/test/fixtures/map-0.2/event-response/respond-tentatively.result.json deleted file mode 100644 index e8ec66b..0000000 --- a/test/fixtures/map-0.2/event-response/respond-tentatively.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001321", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000004", - "descriptionDigest": "sha-256:e64052d2c3692495f7dafdd5b35e302ed54a1bc109e9a1eb95fa3882c27a02b8", - "type": { - "id": "https://mailschema.org/types/event-response", - "version": "0.1", - "contractDigest": "sha-256:43ef4f683ae7f99b056c983439d84e2906aae25bbd6ccf0cefc199592fc8bdca" - }, - "operation": "respond-tentatively", - "state": "completed", - "target": { - "id": "https://calendar.example.net/events/workshop-2026-10", - "revision": "2", - "title": "Design systems workshop", - "digest": "sha-256:8f0e5f23721afc145bd03629c9353659080ffbc397119aeae6d52c19be471616" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://calendar.example.net/map/c/Wf2kR8xLq5Nv1Tz7Mb4Hc9Ds3Pj6Ya0G/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001321", - "output": { - "participationStatus": "tentative" - } -} diff --git a/test/fixtures/map-0.2/information-request/decline.request.json b/test/fixtures/map-0.2/information-request/decline.request.json deleted file mode 100644 index 06e9c5b..0000000 --- a/test/fixtures/map-0.2/information-request/decline.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001211", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "descriptionDigest": "sha-256:4b92f7af35173cae33a5fa75b04418b285a28ec4ea2f4162267afc5142d5fd24", - "type": { - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "contractDigest": "sha-256:66fbd4bc8b15264319ec9a843c8055399a734a412d23e8e2de1562aef8578ac9" - }, - "operation": "decline", - "input": {} -} diff --git a/test/fixtures/map-0.2/information-request/decline.result.json b/test/fixtures/map-0.2/information-request/decline.result.json deleted file mode 100644 index 72f4ea9..0000000 --- a/test/fixtures/map-0.2/information-request/decline.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001211", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "descriptionDigest": "sha-256:4b92f7af35173cae33a5fa75b04418b285a28ec4ea2f4162267afc5142d5fd24", - "type": { - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "contractDigest": "sha-256:66fbd4bc8b15264319ec9a843c8055399a734a412d23e8e2de1562aef8578ac9" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://directory.example.org/listings/acme/requests/contact", - "revision": "1", - "title": "Confirm contact details for Acme", - "digest": "sha-256:f776a125132a267ddcdc99eab7e8f54116fd716ec0058f1849647bfd2654816f" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://directory.example.org/map/c/Hq7vK2mNw9Xb4Rt8Lp3sYd6fJc1gZa0e/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001211", - "output": { - "decision": "declined" - } -} diff --git a/test/fixtures/map-0.2/information-request/description.json b/test/fixtures/map-0.2/information-request/description.json deleted file mode 100644 index 9e0e82a..0000000 --- a/test/fixtures/map-0.2/information-request/description.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "contractDigest": "sha-256:66fbd4bc8b15264319ec9a843c8055399a734a412d23e8e2de1562aef8578ac9" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://directory.example.org/service", - "name": "Example Company Directory", - "authority": "possession", - "execution": { - "url": "https://directory.example.org/map/c/Hq7vK2mNw9Xb4Rt8Lp3sYd6fJc1gZa0e", - "resultUrlTemplate": "https://directory.example.org/map/c/Hq7vK2mNw9Xb4Rt8Lp3sYd6fJc1gZa0e/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://directory.example.org/listings/acme/confirm" - }, - "recipient": "ops@acme.example", - "target": { - "id": "https://directory.example.org/listings/acme/requests/contact", - "revision": "1", - "title": "Confirm contact details for Acme", - "digest": "sha-256:f776a125132a267ddcdc99eab7e8f54116fd716ec0058f1849647bfd2654816f" - }, - "details": { - "purpose": "Keep the public contact details on the Acme listing current.", - "fields": { - "type": "object", - "properties": { - "supportEmail": { - "type": "string", - "title": "Support email address", - "format": "email", - "autocomplete": "work email" - }, - "website": { - "type": "string", - "title": "Website", - "format": "uri", - "autocomplete": "url" - }, - "employees": { - "type": "integer", - "title": "Number of employees", - "minimum": 1, - "maximum": 1000000 - } - }, - "required": [ - "supportEmail", - "website" - ] - } - }, - "operations": [ - { - "id": "submit-response", - "name": "Submit response", - "description": "Send the requested contact details." - }, - { - "id": "decline", - "name": "Decline", - "description": "Decline to provide the details." - } - ] -} diff --git a/test/fixtures/map-0.2/information-request/invalid-request.problem.json b/test/fixtures/map-0.2/information-request/invalid-request.problem.json deleted file mode 100644 index 28c0063..0000000 --- a/test/fixtures/map-0.2/information-request/invalid-request.problem.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "https://mailschema.org/problems/invalid-request", - "title": "Invalid request input", - "status": 400, - "detail": "The input does not satisfy the operation contract.", - "instance": "https://directory.example.org/map/c/Hq7vK2mNw9Xb4Rt8Lp3sYd6fJc1gZa0e/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001201", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001201", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "code": "invalid-request", - "errors": [ - { - "detail": "must match 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])?)*$\"", - "pointer": "/values/supportEmail" - } - ] -} diff --git a/test/fixtures/map-0.2/information-request/submit-response.request.json b/test/fixtures/map-0.2/information-request/submit-response.request.json deleted file mode 100644 index e8076da..0000000 --- a/test/fixtures/map-0.2/information-request/submit-response.request.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001201", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "descriptionDigest": "sha-256:4b92f7af35173cae33a5fa75b04418b285a28ec4ea2f4162267afc5142d5fd24", - "type": { - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "contractDigest": "sha-256:66fbd4bc8b15264319ec9a843c8055399a734a412d23e8e2de1562aef8578ac9" - }, - "operation": "submit-response", - "input": { - "values": { - "supportEmail": "support@acme.example", - "website": "https://acme.example", - "employees": 42 - } - } -} diff --git a/test/fixtures/map-0.2/information-request/submit-response.result.json b/test/fixtures/map-0.2/information-request/submit-response.result.json deleted file mode 100644 index 932efc8..0000000 --- a/test/fixtures/map-0.2/information-request/submit-response.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001201", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000003", - "descriptionDigest": "sha-256:4b92f7af35173cae33a5fa75b04418b285a28ec4ea2f4162267afc5142d5fd24", - "type": { - "id": "https://mailschema.org/types/information-request", - "version": "0.1", - "contractDigest": "sha-256:66fbd4bc8b15264319ec9a843c8055399a734a412d23e8e2de1562aef8578ac9" - }, - "operation": "submit-response", - "state": "accepted", - "target": { - "id": "https://directory.example.org/listings/acme/requests/contact", - "revision": "1", - "title": "Confirm contact details for Acme", - "digest": "sha-256:f776a125132a267ddcdc99eab7e8f54116fd716ec0058f1849647bfd2654816f" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://directory.example.org/map/c/Hq7vK2mNw9Xb4Rt8Lp3sYd6fJc1gZa0e/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001201", - "output": { - "responseRecorded": true - } -} diff --git a/test/fixtures/map-0.2/jcs-vectors.json b/test/fixtures/map-0.2/jcs-vectors.json deleted file mode 100644 index 1e33dea..0000000 --- a/test/fixtures/map-0.2/jcs-vectors.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "name": "member ordering", - "json": "{\"b\":2,\"a\":1,\"aa\":3,\"A\":4,\"é\":5}", - "canonical": "{\"A\":4,\"a\":1,\"aa\":3,\"b\":2,\"é\":5}", - "digest": "sha-256:ddbc230ff782f59d30f388932a6596b2bbb0510e4bb4883a91f579f0b2afb56b" - }, - { - "name": "member ordering by UTF-16 code units", - "json": "{\"\\u20ac\":\"Euro Sign\",\"\\r\":\"Carriage Return\",\"\\ufb33\":\"Hebrew Letter Dalet With Dagesh\",\"1\":\"One\",\"\\ud83d\\ude00\":\"Emoji: Grinning Face\",\"\\u0080\":\"Control\",\"\\u00f6\":\"Latin Small Letter O With Diaeresis\"}", - "canonical": "{\"\\r\":\"Carriage Return\",\"1\":\"One\",\"€\":\"Control\",\"ö\":\"Latin Small Letter O With Diaeresis\",\"€\":\"Euro Sign\",\"😀\":\"Emoji: Grinning Face\",\"דּ\":\"Hebrew Letter Dalet With Dagesh\"}", - "digest": "sha-256:5e321556d22018a9656991a9e94f77ec175fa193e52a2429d312f8419ec8b08c" - }, - { - "name": "string escapes", - "json": "{\"s\":\"quote\\\" backslash\\\\ tab\\t line\\n nul\\u0000 bell\\u0007\"}", - "canonical": "{\"s\":\"quote\\\" backslash\\\\ tab\\t line\\n nul\\u0000 bell\\u0007\"}", - "digest": "sha-256:a0859d9bd34e9fe424424fee1947f6cececab32b675312ab80afdeff2da421df" - }, - { - "name": "unicode is not normalised", - "json": "{\"s\":\"café ☕ 𝄞 e\\u0301\"}", - "canonical": "{\"s\":\"café ☕ 𝄞 é\"}", - "digest": "sha-256:b2dfcf2c8457081b45e99535ceee25907d48f3df2daf00e4faf5033643017733" - }, - { - "name": "integers", - "json": "{\"n\":[0,-0,1,-1,9007199254740991,-9007199254740991]}", - "canonical": "{\"n\":[0,0,1,-1,9007199254740991,-9007199254740991]}", - "digest": "sha-256:823a8412f1f3318eb3aaa56dd79a435d67e455738a84348efa5124457d90357a" - }, - { - "name": "numbers", - "json": "{\"n\":[1.5,1e21,1e-7,0.1,100,-0.5,1E2]}", - "canonical": "{\"n\":[1.5,1e+21,1e-7,0.1,100,-0.5,100]}", - "digest": "sha-256:36cff5012c572fc6d4f147e26e50a2e21b011eaac8c004594aa168e4002cc2e9" - }, - { - "name": "shortest round-trip numbers", - "json": "{\"n\":[0.30000000000000004,1.2345678901234567,5e-324,1.7976931348623157e308,4.35,0.000001,123e-20,333333333.3333333]}", - "canonical": "{\"n\":[0.30000000000000004,1.2345678901234567,5e-324,1.7976931348623157e+308,4.35,0.000001,1.23e-18,333333333.3333333]}", - "digest": "sha-256:1e74e12ecd69f643e738f639208cac4e9f80ebc7994318786dbcb667e933d42d" - }, - { - "name": "nesting and empties", - "json": "{\"a\":[],\"b\":{},\"c\":[{\"z\":null,\"y\":true,\"x\":false}]}", - "canonical": "{\"a\":[],\"b\":{},\"c\":[{\"x\":false,\"y\":true,\"z\":null}]}", - "digest": "sha-256:50e0ce9f658b6a6ccbd27a7541442b682be84a1a5a2a1c066e50512abebb2797" - }, - { - "name": "Email Confirmation description", - "json": "{\"@context\":\"https://mailschema.org/contexts/map-0.2.jsonld\",\"@type\":\"MailAction\",\"@id\":\"urn:uuid:01926a00-0000-7000-8000-000000000009\",\"profile\":\"https://mailschema.org/profiles/map/0.2\",\"type\":{\"id\":\"https://mailschema.org/types/email-confirmation\",\"version\":\"0.1\",\"contractDigest\":\"sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3\"},\"describedAt\":\"2026-09-25T08:00:00Z\",\"expiresAt\":\"2026-10-02T08:00:00Z\",\"service\":{\"id\":\"https://accounts.example.com/service\",\"name\":\"Example Developer Tools\",\"authority\":\"possession\",\"execution\":{\"url\":\"https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG\",\"resultUrlTemplate\":\"https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG/results/{requestId}\",\"resultRetentionSeconds\":604800},\"humanUrl\":\"https://accounts.example.com/signup/pending\"},\"recipient\":\"alex+devtools-7f3a@example.org\",\"target\":{\"id\":\"https://accounts.example.com/pending/signup-5520\",\"revision\":\"1\",\"title\":\"Confirm your new account\",\"digest\":\"sha-256:8fcc87f86199cbd5c4316d65bdca185cf3a232ab1ed43454ec0dfacaa2cfd216\"},\"details\":{\"origin\":\"https://devtools.example.com\",\"address\":\"alex+devtools-7f3a@example.org\",\"purpose\":\"account-creation\",\"requestedAt\":\"2026-09-25T07:58:00Z\"},\"operations\":[{\"id\":\"confirm\",\"name\":\"Confirm\",\"description\":\"Confirm this address and create the account.\"},{\"id\":\"report-unrecognized\",\"name\":\"Report unrecognized\",\"description\":\"Report that you did not sign up.\"}]}", - "canonical": "{\"@context\":\"https://mailschema.org/contexts/map-0.2.jsonld\",\"@id\":\"urn:uuid:01926a00-0000-7000-8000-000000000009\",\"@type\":\"MailAction\",\"describedAt\":\"2026-09-25T08:00:00Z\",\"details\":{\"address\":\"alex+devtools-7f3a@example.org\",\"origin\":\"https://devtools.example.com\",\"purpose\":\"account-creation\",\"requestedAt\":\"2026-09-25T07:58:00Z\"},\"expiresAt\":\"2026-10-02T08:00:00Z\",\"operations\":[{\"description\":\"Confirm this address and create the account.\",\"id\":\"confirm\",\"name\":\"Confirm\"},{\"description\":\"Report that you did not sign up.\",\"id\":\"report-unrecognized\",\"name\":\"Report unrecognized\"}],\"profile\":\"https://mailschema.org/profiles/map/0.2\",\"recipient\":\"alex+devtools-7f3a@example.org\",\"service\":{\"authority\":\"possession\",\"execution\":{\"resultRetentionSeconds\":604800,\"resultUrlTemplate\":\"https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG/results/{requestId}\",\"url\":\"https://accounts.example.com/map/c/Kc4nV7xP2mR9wL1tB5hQ8sD3fZ6yJ0aG\"},\"humanUrl\":\"https://accounts.example.com/signup/pending\",\"id\":\"https://accounts.example.com/service\",\"name\":\"Example Developer Tools\"},\"target\":{\"digest\":\"sha-256:8fcc87f86199cbd5c4316d65bdca185cf3a232ab1ed43454ec0dfacaa2cfd216\",\"id\":\"https://accounts.example.com/pending/signup-5520\",\"revision\":\"1\",\"title\":\"Confirm your new account\"},\"type\":{\"contractDigest\":\"sha-256:f7ba22e7d20ae004d248655e2c744eb1a131b3d1b88de77525551ce70ac131e3\",\"id\":\"https://mailschema.org/types/email-confirmation\",\"version\":\"0.1\"}}", - "digest": "sha-256:a38fcb9a3a5f4389956d4563f62ff29bc2a8bcd25c70b3f651e311ab47814bf0" - } -] diff --git a/test/fixtures/map-0.2/lexical-vectors.json b/test/fixtures/map-0.2/lexical-vectors.json deleted file mode 100644 index fad8f01..0000000 --- a/test/fixtures/map-0.2/lexical-vectors.json +++ /dev/null @@ -1,2618 +0,0 @@ -[ - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00.1Z", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00.123+05:30", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2024-02-29T00:00:00Z", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2000-02-29T00:00:00-12:00", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25 08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00+01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00+0100", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": " 2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "-2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25t08:00:00z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00.0001Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-02-29T00:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "1900-02-29T00:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-24T23:59:60Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-13-01T00:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-31T00:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T24:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00+24:00", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "\n2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x\n2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "\r2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x\r2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "\r\n2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x\r\n2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "…2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x…2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "
2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x
2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "2026-09-25T08:00:00Z
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "
2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "dateTime", - "value": "x
2026-09-25T08:00:00Z", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2024-02-29", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2000-02-29", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": " 2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "-2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-02-29", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "1900-02-29", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-1-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-04-31", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "\n2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x\n2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "\r2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x\r2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "\r\n2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x\r\n2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "…2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x…2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "
2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x
2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "2026-01-01
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "
2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "date", - "value": "x
2026-01-01", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex+devtools-7f3a@example.org", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@localhost", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "first.last@sub.example.co", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "aléx@example.org", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "用户@xn--bcher-kva.example", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a😀@example.org", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a…b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a
b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a​b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a‮b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a⁦b@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "\"a b\"@example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@[127.0.0.1]", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@-example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@example..com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@example.com.", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@bücher.example", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a b@example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "@example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "a@example.com\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "\nalex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x\nalex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "\ralex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x\ralex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "\r\nalex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x\r\nalex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "…alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x…alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "
alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x
alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "alex@example.org
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "
alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "address", - "value": "x
alex@example.org", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "mailto:a@example.com", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/a%20b", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/?a[]=1", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/?q=a|b", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/?q=a^b", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/?q={x}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x:", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/a b", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "no-scheme", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/%zz", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "\nhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x\nhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "\rhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x\rhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "\r\nhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x\r\nhttps://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "…https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x…https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "
https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x
https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "https://example.com/
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "
https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "identifier", - "value": "x
https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com:8443/path?q=1#f", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://xn--bcher-kva.example/", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://app.nitrosend.test/my/map", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://3com.com/", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "http://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com:port/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com:99999/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://1.2.3.256/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://1.2.3.4/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://exa%00mple.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://user@example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://-example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com:0/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com/
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example..com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com./", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "\nhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x\nhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "\rhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x\rhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "\r\nhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x\r\nhttps://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "…https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x…https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "
https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x
https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "https://example.com
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "
https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsIdentifier", - "value": "x
https://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com:8443/map/actions?tenant=1", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/#section", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/map/actions#run", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "http://api.example.com/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "\nhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x\nhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "\rhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x\rhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "\r\nhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x\r\nhttps://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "…https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x…https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "
https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x
https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "https://api.example.com/v1/my/map
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "
https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsTarget", - "value": "x
https://api.example.com/v1/my/map", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://example.com:8443", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://example.com/", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://example.com:99999", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "http://example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "\nhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x\nhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "\rhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x\rhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "\r\nhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x\r\nhttps://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "…https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x…https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "
https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x
https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "https://devtools.example.com
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "
https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "httpsOrigin", - "value": "x
https://devtools.example.com", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/r?id={requestId}", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/r/{requestId}#result", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/r/
{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/r#{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com#{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/{request.Id}{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/{requestId}/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/results", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com/r/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://example.com{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "\nhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x\nhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "\rhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x\rhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "\r\nhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x\r\nhttps://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "…https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x…https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "
https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x
https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "https://reviews.example/map/results/{requestId}
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "
https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "urlTemplate", - "value": "x
https://reviews.example/map/results/{requestId}", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "request-changes", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "a1-b2", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "Approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "-approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve-", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve--now", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve now", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "1approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "\napprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x\napprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "\rapprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x\rapprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "\r\napprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x\r\napprove", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "…approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x…approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "
approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x
approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "approve
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "
approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "token", - "value": "x
approve", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "SHA-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "\rsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x\rsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "\r\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x\r\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "…sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x…sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "digest", - "value": "x
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:3f2504e0-4f89-41d3-9a0c-0305e82c3301", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "URN:UUID:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926A00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-0000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-c000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "\nurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x\nurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "\rurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x\rurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "\r\nurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x\r\nurn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "…urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x…urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "
urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x
urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "urn:uuid:01926a00-0000-7000-8000-000000000001
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "
urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "uuidUrn", - "value": "x
urn:uuid:01926a00-0000-7000-8000-000000000001", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "\rsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x\rsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "\r\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x\r\nsha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "…sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x…sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "contractDigest", - "value": "x
sha-256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "authorizationDetails", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "seats_1", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "1a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "_a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a-b", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "prix€", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "\ra", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x\ra", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "\r\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x\r\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "…a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x…a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "a
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "detailsKey", - "value": "x
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "a", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": " a ", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "é", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "😀", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": " ", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": " ", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "\t\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": "", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "definition": "nonBlank", - "value": " ", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "companyName", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "aBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "A", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "1a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "company_name", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "company-name", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "\ra", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x\ra", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "\r\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x\r\na", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "…a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x…a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "a
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "fieldName", - "value": "x
a", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "shipping postal-code", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "section-a1 billing work tel", - "valid": true - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "Email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "new-password", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "billing cc-number", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "section- email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "home tel", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "\nemail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x\nemail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email\r", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "\remail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x\remail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email\r\n", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "\r\nemail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x\r\nemail", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email…", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "…email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x…email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "
email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x
email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "email
", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "
email", - "valid": false - }, - { - "schema": "https://mailschema.org/schemas/forms-0.1.schema.json", - "definition": "autocomplete", - "value": "x
email", - "valid": false - } -] diff --git a/test/fixtures/map-0.2/media-type-vectors.json b/test/fixtures/map-0.2/media-type-vectors.json deleted file mode 100644 index 7a1acec..0000000 --- a/test/fixtures/map-0.2/media-type-vectors.json +++ /dev/null @@ -1,86 +0,0 @@ -[ - { - "contentType": "application/json", - "accepted": true - }, - { - "contentType": "Application/JSON", - "accepted": true - }, - { - "contentType": "application/json; charset=utf-8", - "accepted": true - }, - { - "contentType": "application/json;charset=\"UTF-8\"", - "accepted": true - }, - { - "contentType": "application/json ;\tcharset=utf-8", - "accepted": true - }, - { - "contentType": " application/json\t", - "accepted": true - }, - { - "contentType": "application/json; charset=utf-8; charset=utf-8", - "accepted": true - }, - { - "contentType": "application/json;", - "accepted": true - }, - { - "contentType": "application/json; ;charset=utf-8", - "accepted": true - }, - { - "contentType": "", - "accepted": false - }, - { - "contentType": "text/plain", - "accepted": false - }, - { - "contentType": "application/jsonx", - "accepted": false - }, - { - "contentType": "application/problem+json", - "accepted": false - }, - { - "contentType": "application/json, text/plain", - "accepted": false - }, - { - "contentType": ";application/json", - "accepted": false - }, - { - "contentType": "application/json; charset=latin1", - "accepted": false - }, - { - "contentType": "application/json; charset=utf8", - "accepted": false - }, - { - "contentType": "application/json; charset = utf-8", - "accepted": false - }, - { - "contentType": "application/json; charset=utf-8; x=1", - "accepted": false - }, - { - "contentType": "application/json; charset=utf-8", - "accepted": false - }, - { - "contentType": "application/json\u000b", - "accepted": false - } -] diff --git a/test/fixtures/map-0.2/meeting-scheduling/already-decided.problem.json b/test/fixtures/map-0.2/meeting-scheduling/already-decided.problem.json deleted file mode 100644 index 9046bc4..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/already-decided.problem.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "https://mailschema.org/problems/already-decided", - "title": "Interaction already decided", - "status": 409, - "detail": "Another request has already decided this interaction.", - "instance": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001402", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001402", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "code": "already-decided" -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/book.request.json b/test/fixtures/map-0.2/meeting-scheduling/book.request.json deleted file mode 100644 index b415d7b..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/book.request.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001401", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "descriptionDigest": "sha-256:0ac3b0399227ddbb80460ebf7b4f088960207e0b063dece1e7e0ce7639c7db5e", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "operation": "book", - "input": { - "slot": "wed-1000", - "values": { - "name": "Alex Doe" - } - } -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/book.result.json b/test/fixtures/map-0.2/meeting-scheduling/book.result.json deleted file mode 100644 index 0db69a3..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/book.result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001401", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "descriptionDigest": "sha-256:0ac3b0399227ddbb80460ebf7b4f088960207e0b063dece1e7e0ce7639c7db5e", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "operation": "book", - "state": "completed", - "target": { - "id": "https://hiring.example.net/interviews/8814", - "revision": "1", - "title": "Interview for the platform engineer role", - "digest": "sha-256:634e324149d969083a079d8881ffd21db0571ea82f7f7515eba33a494820a5f6" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001401", - "output": { - "uid": "wed-1000.01926a00-0000-7000-8000-000000000005@hiring.example.net", - "start": "2026-09-30T10:00:00Z", - "end": "2026-09-30T10:45:00Z" - } -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/book.unavailable.json b/test/fixtures/map-0.2/meeting-scheduling/book.unavailable.json deleted file mode 100644 index a15e776..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/book.unavailable.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001401", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "descriptionDigest": "sha-256:0ac3b0399227ddbb80460ebf7b4f088960207e0b063dece1e7e0ce7639c7db5e", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "operation": "book", - "state": "failed", - "target": { - "id": "https://hiring.example.net/interviews/8814", - "revision": "1", - "title": "Interview for the platform engineer role", - "digest": "sha-256:634e324149d969083a079d8881ffd21db0571ea82f7f7515eba33a494820a5f6" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001401", - "reason": "unavailable", - "output": {} -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/decline.request.json b/test/fixtures/map-0.2/meeting-scheduling/decline.request.json deleted file mode 100644 index 9cb1b3f..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/decline.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001411", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "descriptionDigest": "sha-256:0ac3b0399227ddbb80460ebf7b4f088960207e0b063dece1e7e0ce7639c7db5e", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "operation": "decline", - "input": {} -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/decline.result.json b/test/fixtures/map-0.2/meeting-scheduling/decline.result.json deleted file mode 100644 index 46300c8..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/decline.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001411", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "descriptionDigest": "sha-256:0ac3b0399227ddbb80460ebf7b4f088960207e0b063dece1e7e0ce7639c7db5e", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://hiring.example.net/interviews/8814", - "revision": "1", - "title": "Interview for the platform engineer role", - "digest": "sha-256:634e324149d969083a079d8881ffd21db0571ea82f7f7515eba33a494820a5f6" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001411", - "output": { - "decision": "declined" - } -} diff --git a/test/fixtures/map-0.2/meeting-scheduling/description.json b/test/fixtures/map-0.2/meeting-scheduling/description.json deleted file mode 100644 index 4c33e68..0000000 --- a/test/fixtures/map-0.2/meeting-scheduling/description.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000005", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/meeting-scheduling", - "version": "0.1", - "contractDigest": "sha-256:e72f70f3b2c459802a61f2edb416a97f74763fc423eca3540d11f96068ed8f25" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://hiring.example.net/service", - "name": "Example Recruiting", - "authority": "possession", - "execution": { - "url": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY", - "resultUrlTemplate": "https://hiring.example.net/map/c/Zt5mQ1vB8nK3xR7wL2pH9sD4fG6jC0aY/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://hiring.example.net/interviews/8814/schedule" - }, - "recipient": "alex@example.org", - "target": { - "id": "https://hiring.example.net/interviews/8814", - "revision": "1", - "title": "Interview for the platform engineer role", - "digest": "sha-256:634e324149d969083a079d8881ffd21db0571ea82f7f7515eba33a494820a5f6" - }, - "details": { - "summary": "A 45-minute video interview for the platform engineer role.", - "slots": [ - { - "id": "tue-1400", - "start": "2026-09-29T14:00:00Z", - "end": "2026-09-29T14:45:00Z" - }, - { - "id": "wed-1000", - "start": "2026-09-30T10:00:00Z", - "end": "2026-09-30T10:45:00Z" - }, - { - "id": "wed-1530", - "start": "2026-09-30T15:30:00Z", - "end": "2026-09-30T16:15:00Z" - } - ], - "attendeeFields": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Your name", - "maxLength": 200, - "autocomplete": "name" - }, - "note": { - "type": "string", - "title": "Anything we should know", - "maxLength": 2000 - } - }, - "required": [ - "name" - ] - } - }, - "operations": [ - { - "id": "book", - "name": "Book", - "description": "Book one of the offered interview times." - }, - { - "id": "decline", - "name": "Decline", - "description": "Decline all of the offered times." - } - ] -} diff --git a/test/fixtures/map-0.2/payment-request/accept.request.json b/test/fixtures/map-0.2/payment-request/accept.request.json deleted file mode 100644 index 173030d..0000000 --- a/test/fixtures/map-0.2/payment-request/accept.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001701", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000008", - "descriptionDigest": "sha-256:6413a5a35e1d6ef46f0a9efc85bc5892373437239f3e9010158cc5db8bfaae38", - "type": { - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "contractDigest": "sha-256:098ee27abc0eea1c577a0bba5182155f1810053625bfeaba14a26e917c22c9bc" - }, - "operation": "accept", - "input": { - "paymentDate": "2026-10-20" - } -} diff --git a/test/fixtures/map-0.2/payment-request/accept.result.json b/test/fixtures/map-0.2/payment-request/accept.result.json deleted file mode 100644 index f269cbd..0000000 --- a/test/fixtures/map-0.2/payment-request/accept.result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001701", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000008", - "descriptionDigest": "sha-256:6413a5a35e1d6ef46f0a9efc85bc5892373437239f3e9010158cc5db8bfaae38", - "type": { - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "contractDigest": "sha-256:098ee27abc0eea1c577a0bba5182155f1810053625bfeaba14a26e917c22c9bc" - }, - "operation": "accept", - "state": "completed", - "target": { - "id": "https://billing.example/requests/INV-2026-0917", - "revision": "1", - "title": "Invoice INV-2026-0917", - "digest": "sha-256:dd5cf3dd69ba166baf3d4160221c2ff8bcf915f6a4f68006d21e59465c918f9b" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://billing.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001701", - "actor": "agent-client-3", - "output": { - "decision": "accepted", - "paymentDate": "2026-10-20" - } -} diff --git a/test/fixtures/map-0.2/payment-request/decline.request.json b/test/fixtures/map-0.2/payment-request/decline.request.json deleted file mode 100644 index b5c841f..0000000 --- a/test/fixtures/map-0.2/payment-request/decline.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001711", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000008", - "descriptionDigest": "sha-256:6413a5a35e1d6ef46f0a9efc85bc5892373437239f3e9010158cc5db8bfaae38", - "type": { - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "contractDigest": "sha-256:098ee27abc0eea1c577a0bba5182155f1810053625bfeaba14a26e917c22c9bc" - }, - "operation": "decline", - "input": { - "reason": "The order was cancelled." - } -} diff --git a/test/fixtures/map-0.2/payment-request/decline.result.json b/test/fixtures/map-0.2/payment-request/decline.result.json deleted file mode 100644 index 37d93c2..0000000 --- a/test/fixtures/map-0.2/payment-request/decline.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001711", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000008", - "descriptionDigest": "sha-256:6413a5a35e1d6ef46f0a9efc85bc5892373437239f3e9010158cc5db8bfaae38", - "type": { - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "contractDigest": "sha-256:098ee27abc0eea1c577a0bba5182155f1810053625bfeaba14a26e917c22c9bc" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://billing.example/requests/INV-2026-0917", - "revision": "1", - "title": "Invoice INV-2026-0917", - "digest": "sha-256:dd5cf3dd69ba166baf3d4160221c2ff8bcf915f6a4f68006d21e59465c918f9b" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://billing.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001711", - "actor": "agent-client-3", - "output": { - "decision": "declined" - } -} diff --git a/test/fixtures/map-0.2/payment-request/description.json b/test/fixtures/map-0.2/payment-request/description.json deleted file mode 100644 index 121364e..0000000 --- a/test/fixtures/map-0.2/payment-request/description.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000008", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/payment-request", - "version": "0.1", - "contractDigest": "sha-256:098ee27abc0eea1c577a0bba5182155f1810053625bfeaba14a26e917c22c9bc" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://billing.example/service", - "name": "Example Billing", - "authority": "credential", - "onBehalfOf": { - "id": "https://billing.example/creditors/northwind", - "name": "Northwind Supplies Ltd" - }, - "resource": "https://billing.example/", - "execution": { - "url": "https://billing.example/map/actions", - "resultUrlTemplate": "https://billing.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://billing.example/invoices/INV-2026-0917" - }, - "target": { - "id": "https://billing.example/requests/INV-2026-0917", - "revision": "1", - "title": "Invoice INV-2026-0917", - "digest": "sha-256:dd5cf3dd69ba166baf3d4160221c2ff8bcf915f6a4f68006d21e59465c918f9b" - }, - "details": { - "creditorName": "Northwind Supplies Ltd", - "creditorAccount": { - "iban": "GB33BUKB20201555555555", - "bic": "BUKBGB22" - }, - "instructedAmount": { - "currency": "EUR", - "amount": "1250.00" - }, - "dueDate": "2026-10-25", - "remittanceInformationUnstructured": "Invoice INV-2026-0917", - "invoice": { - "id": "INV-2026-0917", - "url": "https://billing.example/invoices/INV-2026-0917" - } - }, - "operations": [ - { - "id": "accept", - "name": "Accept", - "description": "Commit to pay invoice INV-2026-0917." - }, - { - "id": "decline", - "name": "Decline", - "description": "Refuse invoice INV-2026-0917." - } - ] -} diff --git a/test/fixtures/map-0.2/subscription-preferences/description.json b/test/fixtures/map-0.2/subscription-preferences/description.json deleted file mode 100644 index fa94e17..0000000 --- a/test/fixtures/map-0.2/subscription-preferences/description.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000006", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/subscription-preferences", - "version": "0.1", - "contractDigest": "sha-256:7d300b52892bb287d370ec20e3fc9c4ce9ee69cff7b25646e23cdc4b3e70569b" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://news.example.com/service", - "name": "Example Newsletters", - "authority": "possession", - "execution": { - "url": "https://news.example.com/map/c/Pd8wX2kL5vN1qT7mB4hR9cS3fJ6yG0aZ", - "resultUrlTemplate": "https://news.example.com/map/c/Pd8wX2kL5vN1qT7mB4hR9cS3fJ6yG0aZ/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://news.example.com/preferences" - }, - "recipient": "alex@example.org", - "target": { - "id": "https://news.example.com/subscribers/4471/updates", - "revision": "3", - "title": "Product updates", - "digest": "sha-256:b8ed03641175c3795cec81bcb280fcf306f4268ccd2ca2ac69ca84ae1ac2b393" - }, - "details": { - "listId": "updates.news.example.com", - "fields": { - "type": "object", - "properties": { - "frequency": { - "type": "string", - "title": "How often", - "oneOf": [ - { - "const": "immediate", - "title": "As they happen" - }, - { - "const": "weekly", - "title": "Weekly digest" - } - ], - "default": "immediate" - }, - "topics": { - "type": "array", - "title": "Topics", - "items": { - "anyOf": [ - { - "const": "product", - "title": "Product news" - }, - { - "const": "events", - "title": "Events" - } - ] - }, - "uniqueItems": true, - "default": [ - "product", - "events" - ] - }, - "paused": { - "type": "boolean", - "title": "Pause all email", - "default": false - } - } - } - }, - "operations": [ - { - "id": "update-preferences", - "name": "Update preferences", - "description": "Change how often and about what we email you." - } - ] -} diff --git a/test/fixtures/map-0.2/subscription-preferences/update-preferences.request.json b/test/fixtures/map-0.2/subscription-preferences/update-preferences.request.json deleted file mode 100644 index 265769d..0000000 --- a/test/fixtures/map-0.2/subscription-preferences/update-preferences.request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001501", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000006", - "descriptionDigest": "sha-256:1805c866d2428ced8c980ffe0ee6eec44100c85ff6e20feaa2fb1d681d5e3a5a", - "type": { - "id": "https://mailschema.org/types/subscription-preferences", - "version": "0.1", - "contractDigest": "sha-256:7d300b52892bb287d370ec20e3fc9c4ce9ee69cff7b25646e23cdc4b3e70569b" - }, - "operation": "update-preferences", - "input": { - "values": { - "frequency": "weekly" - } - } -} diff --git a/test/fixtures/map-0.2/subscription-preferences/update-preferences.result.json b/test/fixtures/map-0.2/subscription-preferences/update-preferences.result.json deleted file mode 100644 index 9f023c7..0000000 --- a/test/fixtures/map-0.2/subscription-preferences/update-preferences.result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001501", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000006", - "descriptionDigest": "sha-256:1805c866d2428ced8c980ffe0ee6eec44100c85ff6e20feaa2fb1d681d5e3a5a", - "type": { - "id": "https://mailschema.org/types/subscription-preferences", - "version": "0.1", - "contractDigest": "sha-256:7d300b52892bb287d370ec20e3fc9c4ce9ee69cff7b25646e23cdc4b3e70569b" - }, - "operation": "update-preferences", - "state": "completed", - "target": { - "id": "https://news.example.com/subscribers/4471/updates", - "revision": "3", - "title": "Product updates", - "digest": "sha-256:b8ed03641175c3795cec81bcb280fcf306f4268ccd2ca2ac69ca84ae1ac2b393" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://news.example.com/map/c/Pd8wX2kL5vN1qT7mB4hR9cS3fJ6yG0aZ/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001501", - "output": { - "effectiveAt": "2026-09-25T09:00:00.000Z" - } -} diff --git a/test/fixtures/map-0.2/task-assignment/accept.request.json b/test/fixtures/map-0.2/task-assignment/accept.request.json deleted file mode 100644 index 7d69b23..0000000 --- a/test/fixtures/map-0.2/task-assignment/accept.request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001601", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "accept", - "input": {} -} diff --git a/test/fixtures/map-0.2/task-assignment/accept.result.json b/test/fixtures/map-0.2/task-assignment/accept.result.json deleted file mode 100644 index 072822a..0000000 --- a/test/fixtures/map-0.2/task-assignment/accept.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001601", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "accept", - "state": "completed", - "target": { - "id": "https://tasks.example/tasks/link-check-3.6.9", - "revision": "0", - "title": "Check links in the 3.6.9 documentation", - "digest": "sha-256:8ca7d3f2097105660a0ed3abcafc91a844aaae316227b84295afa8d993112d45" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://tasks.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001601", - "actor": "agent-client-3", - "output": { - "participationStatus": "accepted" - } -} diff --git a/test/fixtures/map-0.2/task-assignment/assignment.ics b/test/fixtures/map-0.2/task-assignment/assignment.ics deleted file mode 100644 index c9fe494..0000000 --- a/test/fixtures/map-0.2/task-assignment/assignment.ics +++ /dev/null @@ -1,14 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Example Tasks//MAP fixture//EN -METHOD:REQUEST -BEGIN:VTODO -UID:link-check-3.6.9@tasks.example -SEQUENCE:0 -DTSTAMP:20260925T080000Z -DUE:20260930T170000Z -SUMMARY:Check links in the 3.6.9 documentation -ORGANIZER:mailto:docs@tasks.example -ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:link-checker@agents.example -END:VTODO -END:VCALENDAR diff --git a/test/fixtures/map-0.2/task-assignment/decline.request.json b/test/fixtures/map-0.2/task-assignment/decline.request.json deleted file mode 100644 index e6c07aa..0000000 --- a/test/fixtures/map-0.2/task-assignment/decline.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001611", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "decline", - "input": { - "reason": "No capacity before the due date." - } -} diff --git a/test/fixtures/map-0.2/task-assignment/decline.result.json b/test/fixtures/map-0.2/task-assignment/decline.result.json deleted file mode 100644 index 7ac322b..0000000 --- a/test/fixtures/map-0.2/task-assignment/decline.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001611", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "decline", - "state": "completed", - "target": { - "id": "https://tasks.example/tasks/link-check-3.6.9", - "revision": "0", - "title": "Check links in the 3.6.9 documentation", - "digest": "sha-256:8ca7d3f2097105660a0ed3abcafc91a844aaae316227b84295afa8d993112d45" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://tasks.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001611", - "actor": "agent-client-3", - "output": { - "participationStatus": "declined" - } -} diff --git a/test/fixtures/map-0.2/task-assignment/description.json b/test/fixtures/map-0.2/task-assignment/description.json deleted file mode 100644 index 8b8f106..0000000 --- a/test/fixtures/map-0.2/task-assignment/description.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "@context": "https://mailschema.org/contexts/map-0.2.jsonld", - "@type": "MailAction", - "@id": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "profile": "https://mailschema.org/profiles/map/0.2", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "describedAt": "2026-09-25T08:00:00Z", - "expiresAt": "2026-10-02T08:00:00Z", - "service": { - "id": "https://tasks.example/service", - "name": "Example Tasks", - "authority": "credential", - "resource": "https://tasks.example/", - "execution": { - "url": "https://tasks.example/map/actions", - "resultUrlTemplate": "https://tasks.example/map/results/{requestId}", - "resultRetentionSeconds": 604800 - }, - "humanUrl": "https://tasks.example/tasks/link-check-3.6.9" - }, - "target": { - "id": "https://tasks.example/tasks/link-check-3.6.9", - "revision": "0", - "title": "Check links in the 3.6.9 documentation", - "digest": "sha-256:8ca7d3f2097105660a0ed3abcafc91a844aaae316227b84295afa8d993112d45" - }, - "details": { - "task": { - "uid": "link-check-3.6.9@tasks.example" - }, - "summary": "Check every link in the 3.6.9 documentation release.", - "due": "2026-09-30T17:00:00Z", - "completionRequirements": "A report listing each broken link and the page it appears on." - }, - "operations": [ - { - "id": "accept", - "name": "Accept", - "description": "Take on the link check." - }, - { - "id": "decline", - "name": "Decline", - "description": "Decline the link check." - }, - { - "id": "report-progress", - "name": "Report progress", - "description": "Report how much of the check is done." - }, - { - "id": "report-completion", - "name": "Report completion", - "description": "Submit the finished report." - } - ] -} diff --git a/test/fixtures/map-0.2/task-assignment/report-completion.request.json b/test/fixtures/map-0.2/task-assignment/report-completion.request.json deleted file mode 100644 index d85d752..0000000 --- a/test/fixtures/map-0.2/task-assignment/report-completion.request.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001631", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "report-completion", - "input": { - "report": "Checked 412 links. Three are broken: two on the install page, one in the changelog.", - "evidence": [ - "https://tasks.example/reports/link-check-3.6.9" - ] - } -} diff --git a/test/fixtures/map-0.2/task-assignment/report-completion.result.json b/test/fixtures/map-0.2/task-assignment/report-completion.result.json deleted file mode 100644 index 9b3a4ca..0000000 --- a/test/fixtures/map-0.2/task-assignment/report-completion.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001631", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "report-completion", - "state": "accepted", - "target": { - "id": "https://tasks.example/tasks/link-check-3.6.9", - "revision": "0", - "title": "Check links in the 3.6.9 documentation", - "digest": "sha-256:8ca7d3f2097105660a0ed3abcafc91a844aaae316227b84295afa8d993112d45" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://tasks.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001631", - "actor": "agent-client-3", - "output": { - "completionRecorded": true - } -} diff --git a/test/fixtures/map-0.2/task-assignment/report-progress.request.json b/test/fixtures/map-0.2/task-assignment/report-progress.request.json deleted file mode 100644 index 9b2d09b..0000000 --- a/test/fixtures/map-0.2/task-assignment/report-progress.request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "kind": "MapRequest", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001621", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "report-progress", - "input": { - "percentComplete": 40 - } -} diff --git a/test/fixtures/map-0.2/task-assignment/report-progress.result.json b/test/fixtures/map-0.2/task-assignment/report-progress.result.json deleted file mode 100644 index 72d959f..0000000 --- a/test/fixtures/map-0.2/task-assignment/report-progress.result.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "kind": "MapResult", - "profile": "https://mailschema.org/profiles/map/0.2", - "requestId": "urn:uuid:01926a00-0000-7000-8000-000000001621", - "interactionId": "urn:uuid:01926a00-0000-7000-8000-000000000007", - "descriptionDigest": "sha-256:62fe35c7b5af6750ca211834928f9555d0a298bd13ea926c7f52d61f999e501f", - "type": { - "id": "https://mailschema.org/types/task-assignment", - "version": "0.1", - "contractDigest": "sha-256:1e30b1cb2fdeb63622a51a49b2782662a4103b47cbcc5e477ed4c25758498f45" - }, - "operation": "report-progress", - "state": "accepted", - "target": { - "id": "https://tasks.example/tasks/link-check-3.6.9", - "revision": "0", - "title": "Check links in the 3.6.9 documentation", - "digest": "sha-256:8ca7d3f2097105660a0ed3abcafc91a844aaae316227b84295afa8d993112d45" - }, - "recordedAt": "2026-09-25T09:00:00.000Z", - "resultUrl": "https://tasks.example/map/results/urn%3Auuid%3A01926a00-0000-7000-8000-000000001621", - "actor": "agent-client-3", - "output": { - "progressRecorded": true - } -} diff --git a/test/fixtures/profile.json b/test/fixtures/profile.json deleted file mode 100644 index 7847a67..0000000 --- a/test/fixtures/profile.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "id": "https://mailschema.org/profiles/map/0.2", - "name": "Mail Action Protocol 0.2", - "status": "working-draft", - "description": "A type-agnostic core: a JSON-LD description in Structured Email, bound by digest to the request that acts on it, executed over HTTPS with credential or possession authority. Types are defined by digest-bound contracts.", - "context": "https://mailschema.org/contexts/map-0.2.jsonld", - "schema": "https://mailschema.org/schemas/map-0.2.schema.json", - "contractFormat": "https://mailschema.org/schemas/type-contract-0.2.schema.json", - "specification": "https://mailschema.org/specification/profile", - "requestMediaType": "application/json", - "resultMediaType": "application/json", - "problemMediaType": "application/problem+json", - "artifacts": { - "context": { - "url": "https://mailschema.org/contexts/map-0.2.jsonld", - "sha256": "dfed0189524d7d16d31ed3d8af97e9a0c458200c4c034b413f9d14fa35b60257" - }, - "schema": { - "url": "https://mailschema.org/schemas/map-0.2.schema.json", - "sha256": "732d60fcb758d945dc15c57d885c9d1c0c76423a7439096d9755e8ec9830836a" - }, - "contractFormat": { - "url": "https://mailschema.org/schemas/type-contract-0.2.schema.json", - "sha256": "e030007cc034662c9d9d448a8a035aff3106cfb96fb60e6aa9f92b99b25df0b9" - }, - "conformance": { - "url": "https://github.com/mailschema/mailschema/blob/main/conformance/map-0.2/manifest.json" - } - } -} diff --git a/test/fixtures/schemas/account-activity-0.1.schema.json b/test/fixtures/schemas/account-activity-0.1.schema.json deleted file mode 100644 index a203f6a..0000000 --- a/test/fixtures/schemas/account-activity-0.1.schema.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/account-activity-0.1.schema.json", - "title": "Account Activity 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/account-activity" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "confirm" - }, - "input": { - "type": "object", - "additionalProperties": false - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "report-unrecognized" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "note": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/action-approval-0.1.schema.json b/test/fixtures/schemas/action-approval-0.1.schema.json deleted file mode 100644 index f9ca08e..0000000 --- a/test/fixtures/schemas/action-approval-0.1.schema.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/action-approval-0.1.schema.json", - "title": "Action Approval 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/action-approval" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "approve" - }, - "input": { - "type": "object", - "additionalProperties": false - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/content-review-0.3.schema.json b/test/fixtures/schemas/content-review-0.3.schema.json deleted file mode 100644 index 1ec2b88..0000000 --- a/test/fixtures/schemas/content-review-0.3.schema.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/content-review-0.3.schema.json", - "title": "Content Review 0.3 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/content-review" - }, - "version": { - "const": "0.3" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "request-changes" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "feedback": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 12000 - } - }, - "required": ["feedback"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "approve" - }, - "input": { - "type": "object", - "additionalProperties": false - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/email-confirmation-0.1.schema.json b/test/fixtures/schemas/email-confirmation-0.1.schema.json deleted file mode 100644 index a2c6ed4..0000000 --- a/test/fixtures/schemas/email-confirmation-0.1.schema.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/email-confirmation-0.1.schema.json", - "title": "Email Confirmation 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/email-confirmation" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "confirm" - }, - "input": { - "type": "object", - "additionalProperties": false - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "report-unrecognized" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "note": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/event-response-0.1.schema.json b/test/fixtures/schemas/event-response-0.1.schema.json deleted file mode 100644 index ce6c7e0..0000000 --- a/test/fixtures/schemas/event-response-0.1.schema.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/event-response-0.1.schema.json", - "title": "Event Response 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/event-response" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "accept" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "comment": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "comment": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "respond-tentatively" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "comment": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/information-request-0.1.schema.json b/test/fixtures/schemas/information-request-0.1.schema.json deleted file mode 100644 index 9cebe0e..0000000 --- a/test/fixtures/schemas/information-request-0.1.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/information-request-0.1.schema.json", - "title": "Information Request 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/information-request" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "submit-response" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "values": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/fieldValues" - } - }, - "required": ["values"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/meeting-scheduling-0.1.schema.json b/test/fixtures/schemas/meeting-scheduling-0.1.schema.json deleted file mode 100644 index f85571f..0000000 --- a/test/fixtures/schemas/meeting-scheduling-0.1.schema.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/meeting-scheduling-0.1.schema.json", - "title": "Meeting Scheduling 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/meeting-scheduling" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "book" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "slot": { - "type": "string", - "pattern": "^[A-Za-z0-9._-]{1,64}$" - }, - "values": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/fieldValues" - } - }, - "required": ["slot"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/payment-request-0.1.schema.json b/test/fixtures/schemas/payment-request-0.1.schema.json deleted file mode 100644 index d29f18b..0000000 --- a/test/fixtures/schemas/payment-request-0.1.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/payment-request-0.1.schema.json", - "title": "Payment Request 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/payment-request" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "accept" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "paymentDate": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/date" - } - }, - "required": ["paymentDate"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - } - ] - } - ] -} diff --git a/test/fixtures/schemas/subscription-preferences-0.1.schema.json b/test/fixtures/schemas/subscription-preferences-0.1.schema.json deleted file mode 100644 index 4a022d0..0000000 --- a/test/fixtures/schemas/subscription-preferences-0.1.schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/subscription-preferences-0.1.schema.json", - "title": "Subscription Preferences 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/subscription-preferences" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "update-preferences" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "values": { - "$ref": "https://mailschema.org/schemas/forms-0.1.schema.json#/$defs/fieldValues" - } - }, - "required": ["values"] - } - } - } - ] -} diff --git a/test/fixtures/schemas/task-assignment-0.1.schema.json b/test/fixtures/schemas/task-assignment-0.1.schema.json deleted file mode 100644 index 8b11f5c..0000000 --- a/test/fixtures/schemas/task-assignment-0.1.schema.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://mailschema.org/schemas/task-assignment-0.1.schema.json", - "title": "Task Assignment 0.1 request", - "allOf": [ - { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/request" - }, - { - "type": "object", - "properties": { - "type": { - "type": "object", - "properties": { - "id": { - "const": "https://mailschema.org/types/task-assignment" - }, - "version": { - "const": "0.1" - } - } - } - } - }, - { - "oneOf": [ - { - "type": "object", - "properties": { - "operation": { - "const": "accept" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "comment": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "decline" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "reason": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - } - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "report-progress" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "percentComplete": { - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "note": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 2000 - } - }, - "required": ["percentComplete"] - } - } - }, - { - "type": "object", - "properties": { - "operation": { - "const": "report-completion" - }, - "input": { - "type": "object", - "additionalProperties": false, - "properties": { - "report": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/nonBlank", - "type": "string", - "minLength": 1, - "maxLength": 12000 - }, - "evidence": { - "type": "array", - "items": { - "$ref": "https://mailschema.org/schemas/map-0.2.schema.json#/$defs/httpsIdentifier" - }, - "maxItems": 20, - "uniqueItems": true - } - }, - "required": ["report"] - } - } - } - ] - } - ] -} diff --git a/test/fixtures/shape-vectors.json b/test/fixtures/shape-vectors.json new file mode 100644 index 0000000..70921f0 --- /dev/null +++ b/test/fixtures/shape-vectors.json @@ -0,0 +1,311 @@ +[ + { + "id": "valid-campaign-send-approval", + "example": "campaign-send-approval", + "edits": [], + "valid": true + }, + { + "id": "valid-publication-approval", + "example": "publication-approval", + "edits": [], + "valid": true + }, + { + "id": "valid-email-address-confirmation", + "example": "email-address-confirmation", + "edits": [], + "valid": true + }, + { + "id": "valid-account-security-response", + "example": "account-security-response", + "edits": [], + "valid": true + }, + { + "id": "old-profile", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/profile", + "value": "https://mailschema.org/profiles/map/0.2" + } + ], + "valid": false + }, + { + "id": "context-array", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/@context", + "value": [ + "https://mailschema.org/contexts/map-0.3.jsonld" + ] + } + ], + "valid": false + }, + { + "id": "inline-context", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/@context", + "value": {} + } + ], + "valid": false + }, + { + "id": "unbound-contract", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/type/contractDigest", + "value": "sha-256:123" + } + ], + "valid": false + }, + { + "id": "core-method", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/method", + "value": "POST" + } + ], + "valid": false + }, + { + "id": "email-endpoint", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/operations/0/url", + "value": "https://evil.example" + } + ], + "valid": false + }, + { + "id": "operation-input", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/operations/0/input", + "value": { + "approve": true + } + } + ], + "valid": false + }, + { + "id": "empty-operations", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/operations", + "value": [] + } + ], + "valid": false + }, + { + "id": "unknown-detail", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/details/endpoint", + "value": "https://evil.example" + } + ], + "valid": false + }, + { + "id": "audience-zero", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/details/audience/count", + "value": 0 + } + ], + "valid": false + }, + { + "id": "audience-fraction", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/details/audience/count", + "value": 1.5 + } + ], + "valid": false + }, + { + "id": "schedule-no-instant", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/details/schedule", + "value": { + "mode": "scheduled" + } + } + ], + "valid": false + }, + { + "id": "terms-not-string", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/terms/version", + "value": 42 + } + ], + "valid": false + }, + { + "id": "insecure-human", + "example": "campaign-send-approval", + "edits": [ + { + "op": "replace", + "path": "/human/url", + "value": "http://service.example/p7" + } + ], + "valid": false + }, + { + "id": "missing-audience-revision", + "example": "campaign-send-approval", + "edits": [ + { + "op": "remove", + "path": "/details/audience/revision" + } + ], + "valid": false + }, + { + "id": "restricted-destination-no-audience", + "example": "publication-approval", + "edits": [ + { + "op": "replace", + "path": "/details/destinations/0/visibility", + "value": "restricted" + } + ], + "valid": false + }, + { + "id": "public-destination-with-audience", + "example": "publication-approval", + "edits": [ + { + "op": "replace", + "path": "/details/destinations/0/audience", + "value": { + "id": "urn:example:readers", + "revision": "r1" + } + } + ], + "valid": false + }, + { + "id": "confirmation-login-purpose", + "example": "email-address-confirmation", + "edits": [ + { + "op": "replace", + "path": "/details/purpose", + "value": "login" + } + ], + "valid": false + }, + { + "id": "capability-extra-input", + "example": "email-address-confirmation", + "edits": [ + { + "op": "replace", + "path": "/operations/0/capability/body", + "value": { + "x": 1 + } + } + ], + "valid": false + }, + { + "id": "security-missing-event", + "example": "account-security-response", + "edits": [ + { + "op": "remove", + "path": "/details/event" + } + ], + "valid": false + }, + { + "id": "ip-literal-service-origin", + "example": "publication-approval", + "edits": [ + { + "op": "replace", + "path": "/service/id", + "value": "https://192.0.2.10" + } + ], + "valid": false + }, + { + "id": "human-url-fragment", + "example": "publication-approval", + "edits": [ + { + "op": "replace", + "path": "/human/url", + "value": "https://service.example/review/p7#approve" + } + ], + "valid": false + }, + { + "id": "issued-at-leap-second", + "example": "publication-approval", + "edits": [ + { + "op": "replace", + "path": "/issuedAt", + "value": "2026-12-31T23:59:60Z" + } + ], + "valid": false + } +] diff --git a/test/new-type.json b/test/new-type.json deleted file mode 100644 index 5121822..0000000 --- a/test/new-type.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "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 d965c1f..5ddb199 100644 --- a/test/package.test.mjs +++ b/test/package.test.mjs @@ -1,158 +1,94 @@ import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; import { readFile, readdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; import test from 'node:test'; import { - Contract, - InvalidContract, - InvalidDocument, - MAP_PROFILE, - assertContribution, canonicalize, + Contract, + coreSchema, + DESCRIPTION_MAX_BYTES, + descriptionErrors, digest, - getContractFormatSchema, - getContributionSchema, - getFormsSchema, - getMapContext, - getMapSchema, - isJsonRequest, - mapErrors, parse, - problem, - problemErrors, - result, - resultStatus, - resultUrl, - settle, - transition, + parseDescription, + PROFILE, } from '../dist/index.js'; -// Internal: the core definitions the shared lexical vectors name. -import { definition } from '../dist/core/artifacts.js'; -const load = async (name) => JSON.parse(await readFile(new URL(name, import.meta.url), 'utf8')); - -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', +const fixture = (name) => new URL(`fixtures/${name}`, import.meta.url); +const load = async (name) => JSON.parse(await readFile(fixture(name), 'utf8')); +const contracts = new Map(); +for (const name of await readdir(fixture('contracts'))) + contracts.set( + name.replace(/-[0-9.]+\.json$/, ''), + Contract.parse(await readFile(fixture(`contracts/${name}`))), ); - 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 a Registry contribution', async () => { - assertContribution(await load('new-type.json')); +test('carries the MAP 0.3 artifacts byte for byte', async () => { + assert.equal(PROFILE, 'https://mailschema.org/profiles/map/0.3'); + assert.equal(coreSchema.$id, 'https://mailschema.org/artifacts/map-0.3/schemas/core.schema.json'); + for (const name of JSON.parse(await readFile(new URL('../artifacts.json', import.meta.url)))) + assert.deepEqual( + await readFile(new URL(`../dist/${name}`, import.meta.url)), + await readFile(new URL(`../src/${name}`, import.meta.url)), + ); }); -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); +test('parses and canonicalizes every JSON vector as MAP requires', async () => { + for (const vector of await load('json-vectors.json')) { + if (!vector.valid) assert.throws(() => parse(vector.json, DESCRIPTION_MAX_BYTES), vector.name); + else + assert.equal(canonicalize(parse(vector.json, DESCRIPTION_MAX_BYTES)), vector.canonical, vector.name); } -}); - -// The MAP core, against the vectors and fixtures every MailSchema implementation shares. -const fixture = (path) => load(`fixtures/${path}`); -const fixtureNames = async (directory) => - (await readdir(new URL(`fixtures/${directory}`, import.meta.url))).filter((name) => - name.endsWith('.json'), - ); - -test('reproduces the shared RFC 8785, I-JSON, lexical and media type vectors', async () => { - for (const vector of await fixture('map-0.2/jcs-vectors.json')) { + for (const vector of await load('jcs-vectors.json')) { assert.equal(canonicalize(JSON.parse(vector.json)), vector.canonical, vector.name); assert.equal(digest(JSON.parse(vector.json)), vector.digest, vector.name); } - for (const vector of await fixture('map-0.2/ijson-vectors.json')) { - const bytes = vector.base64 - ? Buffer.from(vector.base64, 'base64') - : Buffer.from(vector.json, 'utf8'); - if (vector.valid) assert.equal(canonicalize(parse(bytes)), vector.canonical, vector.name); - else assert.throws(() => parse(bytes), InvalidDocument, vector.name); - } - for (const vector of await fixture('map-0.2/lexical-vectors.json')) - assert.equal( - definition(vector.schema, vector.definition)(vector.value), - vector.valid, - `${vector.definition} ${JSON.stringify(vector.value)}`, - ); - for (const vector of await fixture('map-0.2/media-type-vectors.json')) - assert.equal(isJsonRequest(vector.contentType), vector.accepted, vector.contentType); }); -test('verifies every contract by its pinned digest and accepts its published documents', async () => { - for (const file of await fixtureNames('contracts')) { - const contract = await fixture(`contracts/${file}`); - const slug = file.replace(/-[0-9.]+\.json$/, ''); - const description = await fixture(`map-0.2/${slug}/description.json`); - const schema = await fixture(`schemas/${contract.requestSchema.url.split('/').at(-1)}`); - const pinned = description.type.contractDigest; - assert.throws(() => new Contract(contract, schema, { digest: `${pinned}0` }), InvalidContract); - const loaded = new Contract(contract, schema, { digest: pinned }); - assert.deepEqual(loaded.descriptionErrors(description), [], slug); - for (const name of await fixtureNames(`map-0.2/${slug}`)) { - const document = await fixture(`map-0.2/${slug}/${name}`); - assert.deepEqual(mapErrors(document), [], name); - if (document.kind === 'MapRequest') - assert.deepEqual(loaded.requestErrors(document), [], name); - if (document.kind === 'MapResult') assert.deepEqual(loaded.resultErrors(document), [], name); +test('agrees with every shape vector', async () => { + for (const vector of await load('shape-vectors.json')) { + const value = await load(`examples/${vector.example}.json`); + for (const edit of vector.edits) { + const segments = edit.path + .split('/') + .slice(1) + .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~')); + const key = segments.pop(); + const target = segments.reduce((node, segment) => node[segment], value); + if (edit.op === 'remove') delete target[key]; + else target[key] = edit.value; } + const contract = contracts.get(vector.example); + const valid = !descriptionErrors(value).length && !contract.detailsErrors(value.details).length; + assert.equal(valid, vector.valid, vector.id); } }); -test('builds only results and problems the core accepts', async () => { - const description = await fixture('map-0.2/content-review/description.json'); - const request = await fixture('map-0.2/content-review/approve.request.json'); - const at = new Date('2026-09-25T09:00:00Z'); - const members = { - target: description.target, - resultUrl: resultUrl(description, request.requestId), - recordedAt: at, - }; - const proposed = result(request, { - ...members, - state: 'approval-required', - approvalUrl: 'https://reviews.example/approvals/1', - actor: 'agent-1', - }); - assert.equal(resultStatus(proposed), 202); - assert.throws(() => result(request, { ...members, state: 'failed' }), TypeError); - const superseded = transition(proposed, { - state: 'failed', - reason: 'superseded', - recordedAt: at, - }); - assert.deepEqual( - [superseded.reason, superseded.actor, superseded.approvalUrl], - ['superseded', 'agent-1', undefined], +test('identifies each contract by the digest its example names', async () => { + for (const [slug, contract] of contracts) { + const description = parseDescription(await readFile(fixture(`examples/${slug}.json`))); + assert.equal(description.type.contractDigest, contract.digest); + assert.deepEqual(contract.descriptionErrors(description), []); + } +}); + +test('checks files from the command line', () => { + const cli = fileURLToPath(new URL('../bin/mailschema.js', import.meta.url)); + const contract = fileURLToPath(fixture('contracts/publication-approval-0.1.json')); + const description = fileURLToPath(fixture('examples/publication-approval.json')); + assert.match( + execFileSync(process.execPath, [cli, 'contract', contract], { encoding: 'utf8' }), + /^Valid type contract .*\nsha-256:[0-9a-f]{64}\n$/, + ); + assert.match( + execFileSync(process.execPath, [cli, 'description', description, '--contract', contract], { + encoding: 'utf8', + }), + /^Valid MAP 0\.3 description /, ); - const expired = settle(proposed, description, new Date(Date.parse(description.expiresAt) + 1)); - assert.deepEqual([expired.state, expired.reason], ['failed', 'expired']); - const stale = problem('stale-target', { - title: 'The target revision is stale', - detail: 'No effect was applied.', - requestId: request.requestId, - interactionId: request.interactionId, - resultUrl: members.resultUrl, - target: description.target, - }); - assert.deepEqual([stale.status, stale.code, problemErrors(stale)], [409, 'stale-target', []]); - assert.throws( - () => - problem('stale-target', { - title: 'Stale', - detail: 'Stale.', - requestId: request.requestId, - interactionId: request.interactionId, - resultUrl: members.resultUrl, - }), - TypeError, + assert.throws(() => + execFileSync(process.execPath, [cli, 'contract', description], { stdio: 'pipe' }), ); }); diff --git a/tsconfig.json b/tsconfig.json index 6c75301..db0acd2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2023", "module": "ESNext", "moduleResolution": "Bundler", "strict": true,