mailschema 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -119
- package/bin/mailschema.js +42 -53
- package/dist/context.jsonld +50 -0
- package/dist/contract.schema.json +206 -0
- package/dist/core/artifacts.d.ts +11 -36
- package/dist/core/artifacts.js +21 -69
- package/dist/core/bundled.d.ts +4 -4
- package/dist/core/bundled.js +7 -7
- package/dist/core/contract.d.ts +59 -56
- package/dist/core/contract.js +224 -298
- package/dist/core/description.d.ts +45 -0
- package/dist/core/description.js +47 -0
- package/dist/core/implementation.d.ts +32 -0
- package/dist/core/implementation.js +25 -0
- package/dist/core/index.d.ts +7 -6
- package/dist/core/index.js +9 -9
- package/dist/core/json.d.ts +23 -0
- package/dist/core/{document.js → json.js} +61 -51
- package/dist/core/patterns.d.ts +12 -0
- package/dist/core/patterns.js +165 -0
- package/dist/core/sha256.d.ts +2 -0
- package/dist/core/sha256.js +59 -0
- package/dist/core.schema.json +210 -0
- package/dist/implementation.schema.json +173 -0
- package/dist/index.d.ts +4 -9
- package/dist/index.js +2 -14
- package/dist/profile.json +27 -0
- package/package.json +9 -10
- package/dist/contribution.schema.json +0 -874
- package/dist/core/binding.d.ts +0 -24
- package/dist/core/binding.js +0 -50
- package/dist/core/document.d.ts +0 -24
- package/dist/core/documents.d.ts +0 -68
- package/dist/core/documents.js +0 -145
- package/dist/core/limits.d.ts +0 -19
- package/dist/core/limits.js +0 -55
- package/dist/core/pointer.d.ts +0 -12
- package/dist/core/pointer.js +0 -48
- package/dist/core/schemas.d.ts +0 -21
- package/dist/core/schemas.js +0 -125
- package/dist/core/types.d.ts +0 -127
- package/dist/core/types.js +0 -2
- package/dist/forms-0.1.schema.json +0 -329
- package/dist/map-0.2.jsonld +0 -91
- package/dist/map-0.2.schema.json +0 -1033
- package/dist/map.d.ts +0 -8
- package/dist/map.js +0 -18
- package/dist/model.d.ts +0 -95
- package/dist/model.js +0 -5
- package/dist/type-contract-0.2.schema.json +0 -179
- package/dist/validation.d.ts +0 -15
- package/dist/validation.js +0 -64
package/dist/core/binding.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { MapDescription } from './types.ts';
|
|
2
|
-
/** The Content-Type of the part that carries a description, labelled with the profile. */
|
|
3
|
-
export declare const DESCRIPTION_MEDIA_TYPE = "application/ld+json; profile=\"https://mailschema.org/profiles/map/0.2\"";
|
|
4
|
-
/**
|
|
5
|
-
* Whether a designated part carries a description of this profile: its media type is
|
|
6
|
-
* application/ld+json, compared case-insensitively, and its profile parameter lists the
|
|
7
|
-
* profile among its space-separated URIs.
|
|
8
|
-
*/
|
|
9
|
-
export declare const isDescriptionPart: (mediaType: string, profileParameter: string | undefined) => boolean;
|
|
10
|
-
/** The path of an absolute URL exactly as written, with no dot segments removed and nothing decoded. */
|
|
11
|
-
export declare const writtenPath: (url: string) => string;
|
|
12
|
-
/** The possession capability: the last segment of the execution URL path, as written. */
|
|
13
|
-
export declare const capability: (description: MapDescription) => string;
|
|
14
|
-
/** The rules a possession description's capability URLs meet when it is issued. */
|
|
15
|
-
export declare function capabilityProblems(description: MapDescription): string[];
|
|
16
|
-
/**
|
|
17
|
-
* Whether a Content-Type admits a request rather than a 415: the media type
|
|
18
|
-
* application/json, compared case-insensitively, with no parameter other than a UTF-8
|
|
19
|
-
* charset. Only HTTP's optional whitespace, spaces and tabs, may surround each part, and
|
|
20
|
-
* empty parameter slots are ignored, as RFC 9110 permits.
|
|
21
|
-
*/
|
|
22
|
-
export declare function isJsonRequest(contentType: string | undefined): boolean;
|
|
23
|
-
/** The result resource of a request: the description's template with the identifier encoded. */
|
|
24
|
-
export declare const resultUrl: (description: MapDescription, requestId: string) => string;
|
package/dist/core/binding.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// The HTTP and Structured Email bindings: capability URLs, the requests the execution URL
|
|
2
|
-
// admits, where results live and which message part carries a description.
|
|
3
|
-
import { MAP_PROFILE } from "./artifacts.js";
|
|
4
|
-
/** The Content-Type of the part that carries a description, labelled with the profile. */
|
|
5
|
-
export const DESCRIPTION_MEDIA_TYPE = `application/ld+json; profile="${MAP_PROFILE}"`;
|
|
6
|
-
/**
|
|
7
|
-
* Whether a designated part carries a description of this profile: its media type is
|
|
8
|
-
* application/ld+json, compared case-insensitively, and its profile parameter lists the
|
|
9
|
-
* profile among its space-separated URIs.
|
|
10
|
-
*/
|
|
11
|
-
export const isDescriptionPart = (mediaType, profileParameter) => mediaType.toLowerCase() === 'application/ld+json' &&
|
|
12
|
-
(profileParameter ?? '').split(/[ \t]+/).includes(MAP_PROFILE);
|
|
13
|
-
/** The path of an absolute URL exactly as written, with no dot segments removed and nothing decoded. */
|
|
14
|
-
export const writtenPath = (url) => /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*([^?#]*)/.exec(url)?.[1] ?? '';
|
|
15
|
-
/** The possession capability: the last segment of the execution URL path, as written. */
|
|
16
|
-
export const capability = (description) => writtenPath(description.service.execution.url).split('/').at(-1) ?? '';
|
|
17
|
-
const UNGUESSABLE = /^[A-Za-z0-9_-]{22,}$/;
|
|
18
|
-
const dotSegment = (url) => writtenPath(url)
|
|
19
|
-
.split('/')
|
|
20
|
-
.some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment));
|
|
21
|
-
/** The rules a possession description's capability URLs meet when it is issued. */
|
|
22
|
-
export function capabilityProblems(description) {
|
|
23
|
-
const { url, resultUrlTemplate } = description.service.execution;
|
|
24
|
-
const issued = capability(description);
|
|
25
|
-
const problems = [];
|
|
26
|
-
if (!UNGUESSABLE.test(issued))
|
|
27
|
-
problems.push('The capability is too short to be unguessable.');
|
|
28
|
-
if ([url, resultUrlTemplate].some(dotSegment))
|
|
29
|
-
problems.push('A capability URL must not contain dot segments.');
|
|
30
|
-
if (!resultUrlTemplate.startsWith(`${url}/`))
|
|
31
|
-
problems.push('The result template must extend the execution URL and its capability.');
|
|
32
|
-
if (description.service.humanUrl.includes(issued))
|
|
33
|
-
problems.push('The human route must not carry the capability.');
|
|
34
|
-
return problems;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Whether a Content-Type admits a request rather than a 415: the media type
|
|
38
|
-
* application/json, compared case-insensitively, with no parameter other than a UTF-8
|
|
39
|
-
* charset. Only HTTP's optional whitespace, spaces and tabs, may surround each part, and
|
|
40
|
-
* empty parameter slots are ignored, as RFC 9110 permits.
|
|
41
|
-
*/
|
|
42
|
-
export function isJsonRequest(contentType) {
|
|
43
|
-
const [type, ...parameters] = (contentType ?? '')
|
|
44
|
-
.split(';')
|
|
45
|
-
.map((part) => part.replace(/^[ \t]+|[ \t]+$/g, '').toLowerCase());
|
|
46
|
-
return (type === 'application/json' &&
|
|
47
|
-
parameters.every((parameter) => parameter === '' || /^charset=(?:utf-8|"utf-8")$/.test(parameter)));
|
|
48
|
-
}
|
|
49
|
-
/** The result resource of a request: the description's template with the identifier encoded. */
|
|
50
|
-
export const resultUrl = (description, requestId) => description.service.execution.resultUrlTemplate.replace('{requestId}', () => encodeURIComponent(requestId));
|
package/dist/core/document.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** A MAP document that is not I-JSON within the core limits. */
|
|
2
|
-
export declare class InvalidDocument extends Error {
|
|
3
|
-
}
|
|
4
|
-
export declare const MAX_BYTES: number;
|
|
5
|
-
export declare const MAX_DEPTH = 32;
|
|
6
|
-
/**
|
|
7
|
-
* Parse a MAP document as I-JSON (RFC 7493) within the core limits: valid UTF-8 with no
|
|
8
|
-
* byte order mark, at most 64 KiB, no duplicate member names, no lone surrogates,
|
|
9
|
-
* noncharacters or U+0000 in any string, every number within ±(2^53−1), and arrays and
|
|
10
|
-
* objects nested at most 32 deep, the outermost counting as one. Text is measured in its
|
|
11
|
-
* UTF-8 bytes; bytes must be strict UTF-8.
|
|
12
|
-
*/
|
|
13
|
-
export declare function parse(input: string | Uint8Array): unknown;
|
|
14
|
-
/**
|
|
15
|
-
* The RFC 8785 canonical form of a JSON value: members sorted by their UTF-16 code units,
|
|
16
|
-
* ECMAScript's minimal string escapes and its number format. An undefined member is
|
|
17
|
-
* left out and an undefined array item is null, as JSON.stringify writes them.
|
|
18
|
-
*/
|
|
19
|
-
export declare function canonicalize(value: unknown): string;
|
|
20
|
-
/**
|
|
21
|
-
* `sha-256:` and the SHA-256 of the RFC 8785 canonical form: the digest MAP uses for
|
|
22
|
-
* descriptions, contracts and pinned schemas.
|
|
23
|
-
*/
|
|
24
|
-
export declare const digest: (value: unknown) => string;
|
package/dist/core/documents.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { InputError, JsonObject, MapDescription, MapProblem, MapRequest, MapResult, ResultState, Target } from './types.ts';
|
|
2
|
-
/** The HTTP status of every MAP problem code, as the core schema fixes it. */
|
|
3
|
-
export declare const PROBLEM_STATUS: {
|
|
4
|
-
readonly 'invalid-request': 400;
|
|
5
|
-
readonly 'authentication-required': 401;
|
|
6
|
-
readonly refused: 403;
|
|
7
|
-
readonly 'result-not-found': 404;
|
|
8
|
-
readonly 'stale-target': 409;
|
|
9
|
-
readonly 'idempotency-conflict': 409;
|
|
10
|
-
readonly 'request-in-progress': 409;
|
|
11
|
-
readonly 'already-decided': 409;
|
|
12
|
-
readonly 'expired-interaction': 410;
|
|
13
|
-
readonly 'unsupported-type': 422;
|
|
14
|
-
readonly 'unsupported-operation': 422;
|
|
15
|
-
};
|
|
16
|
-
export type ProblemCode = keyof typeof PROBLEM_STATUS;
|
|
17
|
-
/**
|
|
18
|
-
* A result for a request, with every correlation member. `approvalUrl` is given exactly for
|
|
19
|
-
* approval-required and `reason` exactly for failed. Like every builder here, it throws
|
|
20
|
-
* rather than return a document the core refuses.
|
|
21
|
-
*/
|
|
22
|
-
export declare function result(request: MapRequest, { state, target, resultUrl, recordedAt, output, reason, approvalUrl, actor, }: {
|
|
23
|
-
state: ResultState;
|
|
24
|
-
target: Target;
|
|
25
|
-
resultUrl: string;
|
|
26
|
-
recordedAt: Date;
|
|
27
|
-
output?: JsonObject;
|
|
28
|
-
reason?: string;
|
|
29
|
-
approvalUrl?: string;
|
|
30
|
-
actor?: string;
|
|
31
|
-
}): MapResult;
|
|
32
|
-
/**
|
|
33
|
-
* The next state of a pending or approval-required result. Every correlation member and the
|
|
34
|
-
* actor stay; the state, reason, output and recording time change.
|
|
35
|
-
*/
|
|
36
|
-
export declare function transition(previous: MapResult, { state, recordedAt, reason, output, }: {
|
|
37
|
-
state: ResultState;
|
|
38
|
-
recordedAt: Date;
|
|
39
|
-
reason?: string;
|
|
40
|
-
output?: JsonObject;
|
|
41
|
-
}): MapResult;
|
|
42
|
-
/**
|
|
43
|
-
* A problem. With `requestId` it is correlated and carries `instance`, `profile`,
|
|
44
|
-
* `requestId`, `interactionId` and `code`; without, it is plain RFC 9457. It stays within
|
|
45
|
-
* the core limits: a long detail is cut, each input error is bounded, and only as many of
|
|
46
|
-
* the first 100 errors as fit within 64 KiB are kept.
|
|
47
|
-
*/
|
|
48
|
-
export declare function problem(code: ProblemCode, { title, detail, requestId, interactionId, resultUrl, target, errors, }: {
|
|
49
|
-
title: string;
|
|
50
|
-
detail: string;
|
|
51
|
-
requestId?: string;
|
|
52
|
-
interactionId?: string;
|
|
53
|
-
resultUrl?: string;
|
|
54
|
-
target?: Target;
|
|
55
|
-
errors?: InputError[];
|
|
56
|
-
}): MapProblem;
|
|
57
|
-
/** The HTTP status of a result: 202 while work is pending, otherwise 200. */
|
|
58
|
-
export declare const resultStatus: (document: MapResult) => 200 | 202;
|
|
59
|
-
/**
|
|
60
|
-
* Results stay retrievable until the later of the interaction's expiry and the retention
|
|
61
|
-
* interval measured from the latest recorded state.
|
|
62
|
-
*/
|
|
63
|
-
export declare const retainUntil: (description: MapDescription, recordedAt: Date) => Date;
|
|
64
|
-
/**
|
|
65
|
-
* An undecided approval ends as failed, with reason expired, at the interaction's expiry,
|
|
66
|
-
* whether or not anyone looks. The settled result, or undefined when nothing changes.
|
|
67
|
-
*/
|
|
68
|
-
export declare function settle(document: MapResult, description: MapDescription, now: Date): MapResult | undefined;
|
package/dist/core/documents.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Building MAP results and problems, and the lifecycle rules every service applies.
|
|
2
|
-
import { MAP_PROFILE, problemErrors, reached, resultErrors } from "./artifacts.js";
|
|
3
|
-
import { problemMembers, withinDocument } from "./limits.js";
|
|
4
|
-
/** The HTTP status of every MAP problem code, as the core schema fixes it. */
|
|
5
|
-
export const PROBLEM_STATUS = {
|
|
6
|
-
'invalid-request': 400,
|
|
7
|
-
'authentication-required': 401,
|
|
8
|
-
refused: 403,
|
|
9
|
-
'result-not-found': 404,
|
|
10
|
-
'stale-target': 409,
|
|
11
|
-
'idempotency-conflict': 409,
|
|
12
|
-
'request-in-progress': 409,
|
|
13
|
-
'already-decided': 409,
|
|
14
|
-
'expired-interaction': 410,
|
|
15
|
-
'unsupported-type': 422,
|
|
16
|
-
'unsupported-operation': 422,
|
|
17
|
-
};
|
|
18
|
-
const PROBLEM_TYPES = 'https://mailschema.org/problems/';
|
|
19
|
-
const NON_TERMINAL = ['pending', 'approval-required'];
|
|
20
|
-
/** The document, once the core definition `check` names accepts it. */
|
|
21
|
-
function built(document, check) {
|
|
22
|
-
const refused = check(document);
|
|
23
|
-
if (refused.length)
|
|
24
|
-
throw new TypeError(`The core refuses this document: ${refused.slice(0, 3).join('; ')}`);
|
|
25
|
-
return document;
|
|
26
|
-
}
|
|
27
|
-
const defined = (document) => Object.fromEntries(Object.entries(document).filter(([, value]) => value !== undefined));
|
|
28
|
-
/** A result with its members in one order, whichever builder made it, checked by the core. */
|
|
29
|
-
const resultDocument = (members) => built(defined({
|
|
30
|
-
kind: 'MapResult',
|
|
31
|
-
profile: MAP_PROFILE,
|
|
32
|
-
requestId: members.requestId,
|
|
33
|
-
interactionId: members.interactionId,
|
|
34
|
-
descriptionDigest: members.descriptionDigest,
|
|
35
|
-
type: members.type,
|
|
36
|
-
operation: members.operation,
|
|
37
|
-
state: members.state,
|
|
38
|
-
target: members.target,
|
|
39
|
-
recordedAt: members.recordedAt,
|
|
40
|
-
resultUrl: members.resultUrl,
|
|
41
|
-
actor: members.actor,
|
|
42
|
-
approvalUrl: members.approvalUrl,
|
|
43
|
-
reason: members.reason,
|
|
44
|
-
output: members.output,
|
|
45
|
-
}), resultErrors);
|
|
46
|
-
/**
|
|
47
|
-
* A result for a request, with every correlation member. `approvalUrl` is given exactly for
|
|
48
|
-
* approval-required and `reason` exactly for failed. Like every builder here, it throws
|
|
49
|
-
* rather than return a document the core refuses.
|
|
50
|
-
*/
|
|
51
|
-
export function result(request, { state, target, resultUrl, recordedAt, output = {}, reason, approvalUrl, actor, }) {
|
|
52
|
-
if ((state === 'approval-required') !== (approvalUrl !== undefined))
|
|
53
|
-
throw new TypeError('approvalUrl is given exactly when the state is approval-required.');
|
|
54
|
-
if ((state === 'failed') !== (reason !== undefined))
|
|
55
|
-
throw new TypeError('reason is given exactly when the state is failed.');
|
|
56
|
-
return resultDocument({
|
|
57
|
-
requestId: request.requestId,
|
|
58
|
-
interactionId: request.interactionId,
|
|
59
|
-
descriptionDigest: request.descriptionDigest,
|
|
60
|
-
type: request.type,
|
|
61
|
-
operation: request.operation,
|
|
62
|
-
state,
|
|
63
|
-
target,
|
|
64
|
-
recordedAt: recordedAt.toISOString(),
|
|
65
|
-
resultUrl,
|
|
66
|
-
actor,
|
|
67
|
-
approvalUrl,
|
|
68
|
-
reason,
|
|
69
|
-
output,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* The next state of a pending or approval-required result. Every correlation member and the
|
|
74
|
-
* actor stay; the state, reason, output and recording time change.
|
|
75
|
-
*/
|
|
76
|
-
export function transition(previous, { state, recordedAt, reason, output = {}, }) {
|
|
77
|
-
if (!NON_TERMINAL.includes(previous.state))
|
|
78
|
-
throw new TypeError(`A ${previous.state} result is terminal.`);
|
|
79
|
-
if (NON_TERMINAL.includes(state))
|
|
80
|
-
throw new TypeError('A transition ends pending work.');
|
|
81
|
-
if ((state === 'failed') !== (reason !== undefined))
|
|
82
|
-
throw new TypeError('reason is given exactly when the state is failed.');
|
|
83
|
-
return resultDocument({
|
|
84
|
-
...previous,
|
|
85
|
-
approvalUrl: undefined,
|
|
86
|
-
state,
|
|
87
|
-
reason,
|
|
88
|
-
output,
|
|
89
|
-
recordedAt: recordedAt.toISOString(),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* A problem. With `requestId` it is correlated and carries `instance`, `profile`,
|
|
94
|
-
* `requestId`, `interactionId` and `code`; without, it is plain RFC 9457. It stays within
|
|
95
|
-
* the core limits: a long detail is cut, each input error is bounded, and only as many of
|
|
96
|
-
* the first 100 errors as fit within 64 KiB are kept.
|
|
97
|
-
*/
|
|
98
|
-
export function problem(code, { title, detail, requestId, interactionId, resultUrl, target, errors, }) {
|
|
99
|
-
const members = problemMembers(title, detail, errors);
|
|
100
|
-
if (interactionId && !requestId)
|
|
101
|
-
throw new TypeError('An interaction is named only with its request.');
|
|
102
|
-
if (requestId && code === 'authentication-required')
|
|
103
|
-
throw new TypeError('authentication-required is never correlated.');
|
|
104
|
-
if (members.errors && !(requestId && code === 'invalid-request'))
|
|
105
|
-
throw new TypeError('Errors belong to a correlated invalid-request.');
|
|
106
|
-
let correlation = {};
|
|
107
|
-
if (requestId) {
|
|
108
|
-
if (!resultUrl)
|
|
109
|
-
throw new TypeError('A correlated problem names its result URL.');
|
|
110
|
-
if (!interactionId && code !== 'result-not-found')
|
|
111
|
-
throw new TypeError('A correlated problem names its interaction.');
|
|
112
|
-
if (code === 'stale-target' && !target)
|
|
113
|
-
throw new TypeError('A stale-target problem carries the current target.');
|
|
114
|
-
correlation = { instance: resultUrl, profile: MAP_PROFILE, requestId, interactionId, code };
|
|
115
|
-
}
|
|
116
|
-
return built(withinDocument(defined({
|
|
117
|
-
type: `${PROBLEM_TYPES}${code}`,
|
|
118
|
-
title: members.title,
|
|
119
|
-
status: PROBLEM_STATUS[code],
|
|
120
|
-
detail: members.detail,
|
|
121
|
-
...correlation,
|
|
122
|
-
target,
|
|
123
|
-
errors: members.errors,
|
|
124
|
-
})), problemErrors);
|
|
125
|
-
}
|
|
126
|
-
/** The HTTP status of a result: 202 while work is pending, otherwise 200. */
|
|
127
|
-
export const resultStatus = (document) => NON_TERMINAL.includes(document.state) ? 202 : 200;
|
|
128
|
-
/**
|
|
129
|
-
* Results stay retrievable until the later of the interaction's expiry and the retention
|
|
130
|
-
* interval measured from the latest recorded state.
|
|
131
|
-
*/
|
|
132
|
-
export const retainUntil = (description, recordedAt) => new Date(Math.max(Date.parse(description.expiresAt), recordedAt.getTime() + description.service.execution.resultRetentionSeconds * 1000));
|
|
133
|
-
/**
|
|
134
|
-
* An undecided approval ends as failed, with reason expired, at the interaction's expiry,
|
|
135
|
-
* whether or not anyone looks. The settled result, or undefined when nothing changes.
|
|
136
|
-
*/
|
|
137
|
-
export function settle(document, description, now) {
|
|
138
|
-
if (document.state !== 'approval-required' || !reached(now, description.expiresAt))
|
|
139
|
-
return;
|
|
140
|
-
return transition(document, {
|
|
141
|
-
state: 'failed',
|
|
142
|
-
reason: 'expired',
|
|
143
|
-
recordedAt: new Date(description.expiresAt),
|
|
144
|
-
});
|
|
145
|
-
}
|
package/dist/core/limits.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { InputError, JsonObject } from './types.ts';
|
|
2
|
-
/** The text, or its first limit - 1 code points and an ellipsis. */
|
|
3
|
-
export declare const cut: (text: string, limit: number) => string;
|
|
4
|
-
/**
|
|
5
|
-
* An input error within the core problem's limits. A pointer too long to report names its
|
|
6
|
-
* nearest ancestor that fits, with a detail that says so, and a detail too long is cut.
|
|
7
|
-
*/
|
|
8
|
-
export declare function inputError(detail: string, pointer: string): InputError;
|
|
9
|
-
/**
|
|
10
|
-
* A problem's title, detail and input errors within the core limits. An empty or overlong
|
|
11
|
-
* title, an empty detail or an empty error list is the caller's mistake.
|
|
12
|
-
*/
|
|
13
|
-
export declare function problemMembers(title: string, detail: string, errors?: InputError[]): {
|
|
14
|
-
title: string;
|
|
15
|
-
detail: string;
|
|
16
|
-
errors: InputError[] | undefined;
|
|
17
|
-
};
|
|
18
|
-
/** The problem with as many of its errors, in order, as fit within the document limit. */
|
|
19
|
-
export declare function withinDocument(problem: JsonObject): JsonObject;
|
package/dist/core/limits.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// MAP documents within the core limits, with lengths counted in code points as JSON
|
|
2
|
-
// Schema counts them.
|
|
3
|
-
import { MAX_BYTES } from "./document.js";
|
|
4
|
-
import { codePoints, within } from "./pointer.js";
|
|
5
|
-
const TITLE = 240;
|
|
6
|
-
const DETAIL = 4000;
|
|
7
|
-
const POINTER = 1000;
|
|
8
|
-
const ERROR_DETAIL = 2000;
|
|
9
|
-
const ERRORS = 100;
|
|
10
|
-
/** The text, or its first limit - 1 code points and an ellipsis. */
|
|
11
|
-
export const cut = (text, limit) => codePoints(text) > limit ? `${[...text].slice(0, limit - 1).join('')}…` : text;
|
|
12
|
-
/**
|
|
13
|
-
* An input error within the core problem's limits. A pointer too long to report names its
|
|
14
|
-
* nearest ancestor that fits, with a detail that says so, and a detail too long is cut.
|
|
15
|
-
*/
|
|
16
|
-
export function inputError(detail, pointer) {
|
|
17
|
-
if (codePoints(pointer) > POINTER)
|
|
18
|
-
return {
|
|
19
|
-
detail: 'A member within this value does not satisfy the contract.',
|
|
20
|
-
pointer: within(pointer, POINTER),
|
|
21
|
-
};
|
|
22
|
-
return { detail: cut(detail, ERROR_DETAIL), pointer };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* A problem's title, detail and input errors within the core limits. An empty or overlong
|
|
26
|
-
* title, an empty detail or an empty error list is the caller's mistake.
|
|
27
|
-
*/
|
|
28
|
-
export function problemMembers(title, detail, errors) {
|
|
29
|
-
if (codePoints(title) < 1 || codePoints(title) > TITLE)
|
|
30
|
-
throw new RangeError(`A problem title is 1 to ${TITLE} characters.`);
|
|
31
|
-
if (!detail)
|
|
32
|
-
throw new RangeError('A problem detail is not empty.');
|
|
33
|
-
if (errors) {
|
|
34
|
-
if (!errors.length)
|
|
35
|
-
throw new RangeError('Errors, when given, name at least one input error.');
|
|
36
|
-
if (errors.some((error) => !error.detail))
|
|
37
|
-
throw new RangeError('An input error has a detail.');
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
title,
|
|
41
|
-
detail: cut(detail, DETAIL),
|
|
42
|
-
errors: errors?.slice(0, ERRORS).map((error) => inputError(error.detail, error.pointer)),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** The problem with as many of its errors, in order, as fit within the document limit. */
|
|
46
|
-
export function withinDocument(problem) {
|
|
47
|
-
const errors = problem.errors;
|
|
48
|
-
if (!errors)
|
|
49
|
-
return problem;
|
|
50
|
-
const kept = [...errors];
|
|
51
|
-
const size = () => new TextEncoder().encode(JSON.stringify({ ...problem, errors: kept })).length;
|
|
52
|
-
while (kept.length > 1 && size() > MAX_BYTES)
|
|
53
|
-
kept.pop();
|
|
54
|
-
return { ...problem, errors: kept };
|
|
55
|
-
}
|
package/dist/core/pointer.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare const escape: (name: string) => string;
|
|
2
|
-
export declare const segments: (pointer: string) => string[];
|
|
3
|
-
export declare const codePoints: (text: string) => number;
|
|
4
|
-
/** The pointer, or its nearest ancestor no longer than limit code points. */
|
|
5
|
-
export declare function within(pointer: string, limit: number): string;
|
|
6
|
-
/** The value a pointer names through objects and arrays, or undefined. */
|
|
7
|
-
export declare function resolve(value: unknown, pointer: string): unknown;
|
|
8
|
-
/**
|
|
9
|
-
* The value a pointer names through object members only, or undefined. The contract
|
|
10
|
-
* rules bind form fields only through object properties, so no binding indexes an array.
|
|
11
|
-
*/
|
|
12
|
-
export declare function member(value: unknown, pointer: string): unknown;
|
package/dist/core/pointer.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// JSON Pointer (RFC 6901). Undefined stands for "nothing there", which no JSON value is.
|
|
2
|
-
const INDEX = /^(?:0|[1-9][0-9]*)$/;
|
|
3
|
-
export const escape = (name) => name.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
4
|
-
export const segments = (pointer) => pointer
|
|
5
|
-
.split('/')
|
|
6
|
-
.slice(1)
|
|
7
|
-
.map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
8
|
-
// Lengths as JSON Schema counts them: in code points, not UTF-16 code units.
|
|
9
|
-
export const codePoints = (text) => [...text].length;
|
|
10
|
-
/** The pointer, or its nearest ancestor no longer than limit code points. */
|
|
11
|
-
export function within(pointer, limit) {
|
|
12
|
-
const tokens = pointer.split('/');
|
|
13
|
-
while (codePoints(tokens.join('/')) > limit)
|
|
14
|
-
tokens.pop();
|
|
15
|
-
return tokens.join('/');
|
|
16
|
-
}
|
|
17
|
-
/** The value a pointer names through objects and arrays, or undefined. */
|
|
18
|
-
export function resolve(value, pointer) {
|
|
19
|
-
let current = value;
|
|
20
|
-
for (const name of segments(pointer)) {
|
|
21
|
-
if (Array.isArray(current)) {
|
|
22
|
-
if (!INDEX.test(name) || Number(name) >= current.length)
|
|
23
|
-
return undefined;
|
|
24
|
-
current = current[Number(name)];
|
|
25
|
-
}
|
|
26
|
-
else if (current && typeof current === 'object' && Object.hasOwn(current, name))
|
|
27
|
-
current = current[name];
|
|
28
|
-
else
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
return current;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* The value a pointer names through object members only, or undefined. The contract
|
|
35
|
-
* rules bind form fields only through object properties, so no binding indexes an array.
|
|
36
|
-
*/
|
|
37
|
-
export function member(value, pointer) {
|
|
38
|
-
let current = value;
|
|
39
|
-
for (const name of segments(pointer)) {
|
|
40
|
-
if (!current ||
|
|
41
|
-
typeof current !== 'object' ||
|
|
42
|
-
Array.isArray(current) ||
|
|
43
|
-
!Object.hasOwn(current, name))
|
|
44
|
-
return undefined;
|
|
45
|
-
current = current[name];
|
|
46
|
-
}
|
|
47
|
-
return current;
|
|
48
|
-
}
|
package/dist/core/schemas.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { JsonObject } from './types.ts';
|
|
2
|
-
/** Every schema object within a schema, the schema itself first. */
|
|
3
|
-
export declare function nodes(schema: unknown): Generator<JsonObject>;
|
|
4
|
-
/** Every value a keyword has in the schema objects within a schema. */
|
|
5
|
-
export declare const values: (schema: unknown, keyword: string) => unknown[];
|
|
6
|
-
/** Rules of a form fields block that JSON Schema cannot state. */
|
|
7
|
-
export declare function formProblems(fields: JsonObject): string[];
|
|
8
|
-
/**
|
|
9
|
-
* The schema the values of a fields block satisfy: only its fields, each with a text
|
|
10
|
-
* field's `format` read as the core lexical form of that name.
|
|
11
|
-
*/
|
|
12
|
-
export declare function fieldValuesSchema(fields: JsonObject): JsonObject;
|
|
13
|
-
/**
|
|
14
|
-
* Why a contract's schemas would resolve anything but a pinned schema, or undefined. No
|
|
15
|
-
* keyword resolves by scope, in the contract's schemas or the pinned ones it supplies. In
|
|
16
|
-
* the contract's own schemas, no $id or $schema appears but at the request schema's root,
|
|
17
|
-
* since it would move references or change the dialect a subschema is read in; no type is
|
|
18
|
-
* a list; autocomplete, the form fields block's annotation, is absent; and every
|
|
19
|
-
* reference is a string naming a schema in the pinned documents, by URL.
|
|
20
|
-
*/
|
|
21
|
-
export declare function referenceProblem(inline: unknown[], requestSchema: JsonObject, pinned: ReadonlyMap<string, JsonObject>, supplied: JsonObject[]): string | undefined;
|
package/dist/core/schemas.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// The schema objects within a schema, the form fields block and the references a contract's
|
|
2
|
-
// schemas make.
|
|
3
|
-
import { CORE_SCHEMA } from "./artifacts.js";
|
|
4
|
-
import { resolve } from "./pointer.js";
|
|
5
|
-
// The keywords whose value is a schema, a list of schemas, or a map from names to schemas.
|
|
6
|
-
// A name in a map is never a keyword, whatever it is called.
|
|
7
|
-
const SUBSCHEMA = [
|
|
8
|
-
'additionalProperties',
|
|
9
|
-
'propertyNames',
|
|
10
|
-
'items',
|
|
11
|
-
'contains',
|
|
12
|
-
'not',
|
|
13
|
-
'if',
|
|
14
|
-
'then',
|
|
15
|
-
'else',
|
|
16
|
-
'unevaluatedItems',
|
|
17
|
-
'unevaluatedProperties',
|
|
18
|
-
'contentSchema',
|
|
19
|
-
];
|
|
20
|
-
const LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
21
|
-
const MAPS = ['properties', 'patternProperties', '$defs', 'dependentSchemas'];
|
|
22
|
-
/** Every schema object within a schema, the schema itself first. */
|
|
23
|
-
export function* nodes(schema) {
|
|
24
|
-
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
25
|
-
return;
|
|
26
|
-
const node = schema;
|
|
27
|
-
yield node;
|
|
28
|
-
for (const keyword of SUBSCHEMA)
|
|
29
|
-
yield* nodes(node[keyword]);
|
|
30
|
-
for (const keyword of LISTS)
|
|
31
|
-
if (Array.isArray(node[keyword]))
|
|
32
|
-
for (const item of node[keyword])
|
|
33
|
-
yield* nodes(item);
|
|
34
|
-
for (const keyword of MAPS) {
|
|
35
|
-
const map = node[keyword];
|
|
36
|
-
if (map && typeof map === 'object' && !Array.isArray(map))
|
|
37
|
-
for (const item of Object.values(map))
|
|
38
|
-
yield* nodes(item);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/** Every value a keyword has in the schema objects within a schema. */
|
|
42
|
-
export const values = (schema, keyword) => [...nodes(schema)].filter((node) => Object.hasOwn(node, keyword)).map((node) => node[keyword]);
|
|
43
|
-
// The core definitions that give a text field's `format` its lexical form.
|
|
44
|
-
const FORMATS = {
|
|
45
|
-
email: 'address',
|
|
46
|
-
uri: 'identifier',
|
|
47
|
-
date: 'date',
|
|
48
|
-
'date-time': 'dateTime',
|
|
49
|
-
};
|
|
50
|
-
/** Rules of a form fields block that JSON Schema cannot state. */
|
|
51
|
-
export function formProblems(fields) {
|
|
52
|
-
const properties = (fields.properties ?? {});
|
|
53
|
-
const problems = (fields.required ?? [])
|
|
54
|
-
.filter((name) => !Object.hasOwn(properties, name))
|
|
55
|
-
.map((name) => `required field ${name} is not defined`);
|
|
56
|
-
for (const [name, field] of Object.entries(properties)) {
|
|
57
|
-
const choices = (field.oneOf ?? field.items?.anyOf ?? []).map((choice) => choice.const);
|
|
58
|
-
if (new Set(choices).size !== choices.length)
|
|
59
|
-
problems.push(`field ${name} repeats a choice`);
|
|
60
|
-
const defaults = field.default === undefined ? [] : [field.default].flat();
|
|
61
|
-
if (choices.length && defaults.some((value) => !choices.includes(value)))
|
|
62
|
-
problems.push(`field ${name} defaults to a value it does not offer`);
|
|
63
|
-
}
|
|
64
|
-
return problems;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The schema the values of a fields block satisfy: only its fields, each with a text
|
|
68
|
-
* field's `format` read as the core lexical form of that name.
|
|
69
|
-
*/
|
|
70
|
-
export function fieldValuesSchema(fields) {
|
|
71
|
-
const properties = Object.fromEntries(Object.entries((fields.properties ?? {})).map(([name, field]) => {
|
|
72
|
-
if (typeof field.format !== 'string')
|
|
73
|
-
return [name, field];
|
|
74
|
-
const { format, ...rest } = field;
|
|
75
|
-
return [name, { ...rest, $ref: `${CORE_SCHEMA}#/$defs/${FORMATS[format]}` }];
|
|
76
|
-
}));
|
|
77
|
-
return { ...fields, properties, additionalProperties: false };
|
|
78
|
-
}
|
|
79
|
-
// A plain JSON Pointer fragment: nothing percent-encoded, nothing to decode.
|
|
80
|
-
const FRAGMENT = /^(?:\/[A-Za-z0-9._~!$&'()*+,;=:@-]*)*$/;
|
|
81
|
-
// Keywords that resolve by scope rather than by a pinned address.
|
|
82
|
-
const UNPINNED = ['$dynamicRef', '$recursiveRef', '$dynamicAnchor', '$recursiveAnchor', '$anchor'];
|
|
83
|
-
/**
|
|
84
|
-
* Why a contract's schemas would resolve anything but a pinned schema, or undefined. No
|
|
85
|
-
* keyword resolves by scope, in the contract's schemas or the pinned ones it supplies. In
|
|
86
|
-
* the contract's own schemas, no $id or $schema appears but at the request schema's root,
|
|
87
|
-
* since it would move references or change the dialect a subschema is read in; no type is
|
|
88
|
-
* a list; autocomplete, the form fields block's annotation, is absent; and every
|
|
89
|
-
* reference is a string naming a schema in the pinned documents, by URL.
|
|
90
|
-
*/
|
|
91
|
-
export function referenceProblem(inline, requestSchema, pinned, supplied) {
|
|
92
|
-
for (const schema of [...inline, requestSchema, ...supplied]) {
|
|
93
|
-
const found = UNPINNED.find((keyword) => values(schema, keyword).length);
|
|
94
|
-
if (found)
|
|
95
|
-
return `${found} is not pinned by any digest`;
|
|
96
|
-
}
|
|
97
|
-
for (const schema of [...inline, requestSchema]) {
|
|
98
|
-
const all = [...nodes(schema)];
|
|
99
|
-
const nested = schema === requestSchema ? all.slice(1) : all;
|
|
100
|
-
if (nested.some((node) => '$id' in node))
|
|
101
|
-
return 'a nested $id would move references';
|
|
102
|
-
if (nested.some((node) => '$schema' in node))
|
|
103
|
-
return 'a nested $schema would change the dialect';
|
|
104
|
-
if (all.some((node) => Array.isArray(node.type)))
|
|
105
|
-
return 'type names one type, never a list';
|
|
106
|
-
if (all.some((node) => 'autocomplete' in node))
|
|
107
|
-
return 'autocomplete belongs to the form fields block';
|
|
108
|
-
for (const ref of values(schema, '$ref')) {
|
|
109
|
-
if (typeof ref !== 'string')
|
|
110
|
-
return 'a $ref is not a string';
|
|
111
|
-
const [base, fragment = ''] = ref.split(/#(.*)/s);
|
|
112
|
-
const document = pinned.get(base);
|
|
113
|
-
if (!document)
|
|
114
|
-
return `${ref} is not a pinned dependency`;
|
|
115
|
-
if (fragment && !fragment.startsWith('/'))
|
|
116
|
-
return `${ref} names an anchor, not a JSON Pointer`;
|
|
117
|
-
if (!FRAGMENT.test(fragment))
|
|
118
|
-
return `${ref} is not a plain JSON Pointer`;
|
|
119
|
-
const target = resolve(document, fragment);
|
|
120
|
-
if (typeof target !== 'boolean' &&
|
|
121
|
-
!(target && typeof target === 'object' && !Array.isArray(target)))
|
|
122
|
-
return `${ref} does not name a schema`;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|