@openvtc/trust-tasks 0.27.7 → 0.27.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/README.md +9 -0
- package/dist/_runtime/codes.d.ts +21 -2
- package/dist/_runtime/codes.d.ts.map +1 -1
- package/dist/_runtime/codes.js +23 -19
- package/dist/_runtime/codes.js.map +1 -1
- package/dist/_runtime/extends.d.ts +36 -8
- package/dist/_runtime/extends.d.ts.map +1 -1
- package/dist/_runtime/extends.js +83 -38
- package/dist/_runtime/extends.js.map +1 -1
- package/dist/_runtime/index.d.ts +2 -1
- package/dist/_runtime/index.d.ts.map +1 -1
- package/dist/_runtime/index.js +2 -1
- package/dist/_runtime/index.js.map +1 -1
- package/dist/_runtime/type-uri.d.ts +96 -0
- package/dist/_runtime/type-uri.d.ts.map +1 -0
- package/dist/_runtime/type-uri.js +194 -0
- package/dist/_runtime/type-uri.js.map +1 -0
- package/dist/index.d.ts +3771 -1257
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3771 -1257
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/_runtime/codes.ts +24 -21
- package/src/_runtime/extends.ts +90 -43
- package/src/_runtime/index.ts +15 -0
- package/src/_runtime/type-uri.ts +252 -0
- package/src/index.ts +3771 -1257
package/src/_runtime/extends.ts
CHANGED
|
@@ -29,14 +29,21 @@
|
|
|
29
29
|
import { EXTENDS_BY_TYPE_URI } from "../_extends.js";
|
|
30
30
|
import { canonicalJson } from "./canonical.js";
|
|
31
31
|
import type { RejectReason, SpecPolicy, TrustTaskDocument } from "./document.js";
|
|
32
|
+
import {
|
|
33
|
+
formatTypeUri,
|
|
34
|
+
sameSpecification,
|
|
35
|
+
tryParseTypeUri,
|
|
36
|
+
typeUriWithVersion,
|
|
37
|
+
type TypeUri,
|
|
38
|
+
} from "./type-uri.js";
|
|
32
39
|
|
|
33
|
-
const PREFIX = "https://trusttasks.org/spec/";
|
|
34
40
|
const VERSION_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
43
|
* Where to look up the version a Type URI declares it extends. Either a map
|
|
38
44
|
* from bare request Type URI to version (the shape of the generated
|
|
39
|
-
* `EXTENDS_BY_TYPE_URI`), or a function. Defaults to the generated index
|
|
45
|
+
* `EXTENDS_BY_TYPE_URI`), or a function. Defaults to the generated index plus
|
|
46
|
+
* the tables an application registered with {@link declareExtends}; pass
|
|
40
47
|
* your own to route on knowledge this library release does not carry, or in a
|
|
41
48
|
* test.
|
|
42
49
|
*/
|
|
@@ -44,27 +51,54 @@ export type ExtendsLookup =
|
|
|
44
51
|
| Readonly<Record<string, string>>
|
|
45
52
|
| ((bareTypeUri: string) => string | null | undefined);
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
/** `M.N` of a parsed Type URI. */
|
|
55
|
+
const versionOf = (u: TypeUri): string => `${u.major}.${u.minor}`;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The key an {@link ExtendsLookup} is consulted with: the bare (fragment-free)
|
|
59
|
+
* request Type URI of `u` at `version`, under `u`'s own authority and form.
|
|
60
|
+
* Building it from `u` rather than from the slug is what keeps a chain inside
|
|
61
|
+
* one authority: a third-party `x/0.3` is never looked up as the registry's
|
|
62
|
+
* `x/0.3`.
|
|
63
|
+
*/
|
|
64
|
+
function bareKey(u: TypeUri): string {
|
|
65
|
+
return formatTypeUri({ ...u, variant: null });
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Tables an application registered with {@link declareExtends}, in the order
|
|
70
|
+
* they were declared.
|
|
71
|
+
*/
|
|
72
|
+
const APPLICATION_EXTENDS: Readonly<Record<string, string>>[] = [];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Teach the default lookup — {@link extendsOf}, {@link extendsChain} and
|
|
76
|
+
* {@link readForSpec} when no `index` is passed — the `extends` declarations
|
|
77
|
+
* of specification versions this package's generated table does not carry:
|
|
78
|
+
* those an application generated itself from registry bundles with
|
|
79
|
+
* `trust-tasks-gen-ts` (`@openvtc/trust-tasks-gen`).
|
|
80
|
+
*
|
|
81
|
+
* Each key is a bare request Type URI and each value the `"MAJOR.MINOR"` it
|
|
82
|
+
* extends — the shape of the generated `EXTENDS_BY_TYPE_URI`. The `index.ts`
|
|
83
|
+
* the generator writes exports a `declareExtends()` that calls this with its
|
|
84
|
+
* table, so an application calls that once at start-up.
|
|
85
|
+
*
|
|
86
|
+
* Process-wide and additive. Declaring the same table twice is harmless, and
|
|
87
|
+
* where this package's generated table already answers for a Type URI that
|
|
88
|
+
* answer stands. A declaration is still only followed when it names a strictly
|
|
89
|
+
* earlier MINOR of the same MAJOR (SPEC §5.2).
|
|
90
|
+
*/
|
|
91
|
+
export function declareExtends(declarations: Readonly<Record<string, string>>): void {
|
|
92
|
+
if (!APPLICATION_EXTENDS.includes(declarations)) APPLICATION_EXTENDS.push(declarations);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hasOwn = (o: object, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k);
|
|
96
|
+
|
|
97
|
+
/** The default {@link ExtendsLookup}: the generated table, then {@link declareExtends}'s. */
|
|
98
|
+
function defaultLookup(bare: string): string | undefined {
|
|
99
|
+
if (hasOwn(EXTENDS_BY_TYPE_URI, bare)) return EXTENDS_BY_TYPE_URI[bare];
|
|
100
|
+
for (const table of APPLICATION_EXTENDS) if (hasOwn(table, bare)) return table[bare];
|
|
101
|
+
return undefined;
|
|
68
102
|
}
|
|
69
103
|
|
|
70
104
|
function lookup(index: ExtendsLookup, bare: string): string | null {
|
|
@@ -77,32 +111,40 @@ function lookup(index: ExtendsLookup, bare: string): string | null {
|
|
|
77
111
|
* `#request` / `#response` fragment is ignored — the declaration is the
|
|
78
112
|
* specification's, covering both variants.
|
|
79
113
|
*/
|
|
80
|
-
export function extendsOf(typeUri: string, index: ExtendsLookup =
|
|
81
|
-
const p =
|
|
114
|
+
export function extendsOf(typeUri: string, index: ExtendsLookup = defaultLookup): string | null {
|
|
115
|
+
const p = tryParseTypeUri(typeUri);
|
|
82
116
|
if (p === null) return null;
|
|
83
|
-
return lookup(index,
|
|
117
|
+
return lookup(index, bareKey(p));
|
|
84
118
|
}
|
|
85
119
|
|
|
86
120
|
/**
|
|
87
121
|
* Every version `typeUri` extends, transitively, nearest first: for 0.3
|
|
88
122
|
* extends 0.2 extends 0.1, `["0.2", "0.1"]`.
|
|
89
123
|
*
|
|
124
|
+
* `typeUri` may be in any SPEC §6.1 form. Each link is looked up under the
|
|
125
|
+
* same authority, form and slug as `typeUri` — only the version changes — so a
|
|
126
|
+
* chain never crosses authorities: a third-party specification's chain is read
|
|
127
|
+
* from its own declarations, never from a registry specification that happens
|
|
128
|
+
* to share its slug.
|
|
129
|
+
*
|
|
90
130
|
* Defensive against a malformed index: the walk stops at a declaration that is
|
|
91
131
|
* not strictly earlier within the same MAJOR (the build refuses those), and at
|
|
92
132
|
* a cycle.
|
|
93
133
|
*/
|
|
94
|
-
export function extendsChain(typeUri: string, index: ExtendsLookup =
|
|
95
|
-
const start =
|
|
134
|
+
export function extendsChain(typeUri: string, index: ExtendsLookup = defaultLookup): string[] {
|
|
135
|
+
const start = tryParseTypeUri(typeUri);
|
|
96
136
|
if (start === null) return [];
|
|
97
137
|
const chain: string[] = [];
|
|
98
138
|
let current = start;
|
|
99
139
|
for (;;) {
|
|
100
|
-
const next = lookup(index,
|
|
140
|
+
const next = lookup(index, bareKey(current));
|
|
101
141
|
if (next === null) break;
|
|
102
|
-
const
|
|
103
|
-
|
|
142
|
+
const m = VERSION_RE.exec(next)!;
|
|
143
|
+
const major = Number(m[1]);
|
|
144
|
+
const minor = Number(m[2]);
|
|
145
|
+
if (major !== current.major || minor >= current.minor) break;
|
|
104
146
|
chain.push(next);
|
|
105
|
-
current =
|
|
147
|
+
current = typeUriWithVersion(current, major, minor);
|
|
106
148
|
}
|
|
107
149
|
return chain;
|
|
108
150
|
}
|
|
@@ -154,13 +196,16 @@ export function receivedDocumentOf<P>(doc: TrustTaskDocument<P>): TrustTaskDocum
|
|
|
154
196
|
* implements (`spec`, a generated `SPEC` or `RESPONSE_SPEC`).
|
|
155
197
|
*
|
|
156
198
|
* - Same Type URI as `spec.typeUri` — accepted unchanged.
|
|
157
|
-
* - Same slug and variant, later version
|
|
158
|
-
* `spec`'s version — accepted, payload pruned
|
|
159
|
-
* with {@link prunePayload}, `type` kept as
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
199
|
+
* - Same specification (authority, form and slug) and variant, later version
|
|
200
|
+
* whose `extends` chain reaches `spec`'s version — accepted, payload pruned
|
|
201
|
+
* against `spec.payloadSchema` with {@link prunePayload}, `type` kept as
|
|
202
|
+
* received.
|
|
203
|
+
* - Same specification and variant, any other version — refused
|
|
204
|
+
* `unsupportedVersion`. (Without a declaration a later MINOR SHOULD NOT be
|
|
205
|
+
* accepted: a draft may break in one.)
|
|
206
|
+
* - A different specification or variant — refused `unsupportedType`. The
|
|
207
|
+
* same slug under another authority is another specification (SPEC §6.1),
|
|
208
|
+
* so a third-party `x/0.3` never routes to a registry `x/0.1`, or back.
|
|
164
209
|
* - Not a Trust Task document at all — refused `malformedRequest`.
|
|
165
210
|
*
|
|
166
211
|
* Proofs are not this function's business, and it neither reads nor alters
|
|
@@ -172,7 +217,7 @@ export function readForSpec<P>(
|
|
|
172
217
|
spec: SpecPolicy,
|
|
173
218
|
options: { index?: ExtendsLookup } = {},
|
|
174
219
|
): ReadForSpecResult<P> {
|
|
175
|
-
const index = options.index ??
|
|
220
|
+
const index = options.index ?? defaultLookup;
|
|
176
221
|
if (!isPlainObject(received) || typeof received.type !== "string" || !("payload" in received)) {
|
|
177
222
|
return refused("malformedRequest", "not a Trust Task document (SPEC §4.2)");
|
|
178
223
|
}
|
|
@@ -181,15 +226,17 @@ export function readForSpec<P>(
|
|
|
181
226
|
return { kind: "accepted", doc: doc as TrustTaskDocument<P>, received: doc, extended: false };
|
|
182
227
|
}
|
|
183
228
|
|
|
184
|
-
const got =
|
|
185
|
-
const want =
|
|
186
|
-
if (got === null || want === null || got
|
|
229
|
+
const got = tryParseTypeUri(doc.type);
|
|
230
|
+
const want = tryParseTypeUri(spec.typeUri);
|
|
231
|
+
if (got === null || want === null || !sameSpecification(got, want) || got.variant !== want.variant) {
|
|
187
232
|
return refused("unsupportedType", `unsupported Trust Task type ${JSON.stringify(doc.type)}`);
|
|
188
233
|
}
|
|
189
|
-
|
|
234
|
+
const gotVersion = versionOf(got);
|
|
235
|
+
const wantVersion = versionOf(want);
|
|
236
|
+
if (gotVersion === wantVersion || !extendsChain(doc.type, index).includes(wantVersion)) {
|
|
190
237
|
return refused(
|
|
191
238
|
"unsupportedVersion",
|
|
192
|
-
`version ${
|
|
239
|
+
`version ${gotVersion} of ${got.slug} is not supported; this consumer implements ${wantVersion}`,
|
|
193
240
|
);
|
|
194
241
|
}
|
|
195
242
|
|
package/src/_runtime/index.ts
CHANGED
|
@@ -68,9 +68,24 @@ export {
|
|
|
68
68
|
type ProofVerifier,
|
|
69
69
|
} from "./consume.js";
|
|
70
70
|
|
|
71
|
+
export {
|
|
72
|
+
FRAMEWORK_RESERVED_SLUGS,
|
|
73
|
+
REGISTRY_TYPE_URI_AUTHORITY,
|
|
74
|
+
TypeUriError,
|
|
75
|
+
formatTypeUri,
|
|
76
|
+
parseTypeUri,
|
|
77
|
+
sameSpecification,
|
|
78
|
+
tryParseTypeUri,
|
|
79
|
+
typeUriWithVersion,
|
|
80
|
+
type TypeUri,
|
|
81
|
+
type TypeUriErrorKind,
|
|
82
|
+
type TypeUriVariant,
|
|
83
|
+
} from "./type-uri.js";
|
|
84
|
+
|
|
71
85
|
export { canonicalJson, sha256Hex } from "./canonical.js";
|
|
72
86
|
|
|
73
87
|
export {
|
|
88
|
+
declareExtends,
|
|
74
89
|
extendsChain,
|
|
75
90
|
extendsOf,
|
|
76
91
|
prunePayload,
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed Trust Task *Type URI* — every form SPEC.md §6.1 defines: the public
|
|
3
|
+
* registry's canonical form, and the two shapes a third-party specification
|
|
4
|
+
* (SPEC.md §6.5) may use under any other authority:
|
|
5
|
+
*
|
|
6
|
+
* ```text
|
|
7
|
+
* https://trusttasks.org/spec/<slug>/<MAJOR.MINOR> public registry
|
|
8
|
+
* <prefix>/spec/<slug>/<MAJOR.MINOR> path form (https, DID URL, …)
|
|
9
|
+
* urn:<NID>:<prefix>:spec:<slug-segments>:<MAJOR.MINOR> URN form
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* each optionally followed by `#request` or `#response` (§4.4.1). A Type URI
|
|
13
|
+
* whose scheme is `urn` must use the URN form, and every other scheme the path
|
|
14
|
+
* form; `http` is refused in every form. {@link formatTypeUri} reproduces the
|
|
15
|
+
* parsed string exactly — a Type URI is a name, compared by its whole string
|
|
16
|
+
* (§6.1), so the authority is kept verbatim and is part of identity: the same
|
|
17
|
+
* slug under two authorities is two specifications.
|
|
18
|
+
*
|
|
19
|
+
* Hand-written. Mirrors `type_uri.rs` in trust-tasks-rs, whose unit tests are
|
|
20
|
+
* the source of most of the vectors in `test/type-uri.test.ts`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The `#request` / `#response` fragment of a Type URI (SPEC §4.4.1). */
|
|
24
|
+
export type TypeUriVariant = "request" | "response";
|
|
25
|
+
|
|
26
|
+
/** A parsed Type URI. Plain data; build a string back with {@link formatTypeUri}. */
|
|
27
|
+
export interface TypeUri {
|
|
28
|
+
/**
|
|
29
|
+
* Everything before `/spec/` (path form) or `:spec:` (URN form), verbatim —
|
|
30
|
+
* e.g. `https://trusttasks.org`, `did:web:acme.example`, `urn:example:tasks`.
|
|
31
|
+
*/
|
|
32
|
+
readonly authority: string;
|
|
33
|
+
/** `"path"` — `<authority>/spec/<slug>/<M.N>`; `"urn"` — `<authority>:spec:<seg>:…:<M.N>`. */
|
|
34
|
+
readonly form: "path" | "urn";
|
|
35
|
+
/** The slug, `/`-separated in both forms (`payments:settle` in a URN is `payments/settle`). */
|
|
36
|
+
readonly slug: string;
|
|
37
|
+
readonly major: number;
|
|
38
|
+
readonly minor: number;
|
|
39
|
+
/** The fragment, or `null` when absent (semantically equivalent to `"request"`, §4.4.1). */
|
|
40
|
+
readonly variant: TypeUriVariant | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Why a string is not a Type URI — the cases of Rust's `ParseTypeUriError`. */
|
|
44
|
+
export type TypeUriErrorKind =
|
|
45
|
+
/** No scheme, a malformed one, or `http` (refused in every form, §6.1). */
|
|
46
|
+
| "scheme"
|
|
47
|
+
/** Neither the path form nor (for `urn:`) the URN form. */
|
|
48
|
+
| "shape"
|
|
49
|
+
/** The slug fails the §6.1 grammar. */
|
|
50
|
+
| "slug"
|
|
51
|
+
/** The slug is in the §6.1 reserved namespace and is not a framework slug. */
|
|
52
|
+
| "reservedSlug"
|
|
53
|
+
/** The version is not `MAJOR.MINOR` (§5.1). */
|
|
54
|
+
| "version"
|
|
55
|
+
/** A fragment other than `#request` / `#response`. */
|
|
56
|
+
| "fragment";
|
|
57
|
+
|
|
58
|
+
/** Thrown by {@link parseTypeUri}. */
|
|
59
|
+
export class TypeUriError extends Error {
|
|
60
|
+
readonly kind: TypeUriErrorKind;
|
|
61
|
+
/** The string that failed to parse (for `slug` / `version` / `fragment`, the offending part). */
|
|
62
|
+
readonly input: string;
|
|
63
|
+
|
|
64
|
+
constructor(kind: TypeUriErrorKind, input: string, message: string) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "TypeUriError";
|
|
67
|
+
this.kind = kind;
|
|
68
|
+
this.input = input;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The public registry's authority. */
|
|
73
|
+
export const REGISTRY_TYPE_URI_AUTHORITY = "https://trusttasks.org";
|
|
74
|
+
|
|
75
|
+
const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*$/;
|
|
76
|
+
const SEGMENT_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
77
|
+
const VERSION_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
78
|
+
const U32_MAX = 0xffff_ffff;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The framework slugs permitted inside the §6.1 reservation. A third
|
|
82
|
+
* hand-maintained copy of `#/properties/slug/anyOf[1]/enum` in
|
|
83
|
+
* `specs/spec.meta.schema.json` (Rust's `is_allowed_framework_slug` is the
|
|
84
|
+
* second); `test/type-uri.test.ts` reads that file and fails if they disagree.
|
|
85
|
+
*/
|
|
86
|
+
export const FRAMEWORK_RESERVED_SLUGS: readonly string[] = Object.freeze([
|
|
87
|
+
"trust-task",
|
|
88
|
+
"trust-task-error",
|
|
89
|
+
"trust-task-ok",
|
|
90
|
+
"trust-task-next-step",
|
|
91
|
+
"trust-task-discovery",
|
|
92
|
+
"trust-task-control",
|
|
93
|
+
"trust-ceremony-receipt",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/** Whether `slug` is in the §6.1 reserved namespace `^trust-(task|ceremony)($|-|/)`. */
|
|
97
|
+
function isReservedNamespace(slug: string): boolean {
|
|
98
|
+
const first = slug.split("/")[0]!;
|
|
99
|
+
return ["trust-task", "trust-ceremony"].some((stem) => first === stem || first.startsWith(`${stem}-`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Throws unless `slug` satisfies the §6.1 grammar and reservation. */
|
|
103
|
+
function checkSlug(slug: string): void {
|
|
104
|
+
if (slug.length === 0 || !slug.split("/").every((s) => SEGMENT_RE.test(s))) {
|
|
105
|
+
throw new TypeUriError("slug", slug, `slug ${JSON.stringify(slug)} does not satisfy SPEC §6.1 grammar`);
|
|
106
|
+
}
|
|
107
|
+
if (isReservedNamespace(slug) && !FRAMEWORK_RESERVED_SLUGS.includes(slug)) {
|
|
108
|
+
throw new TypeUriError("reservedSlug", slug, `slug ${JSON.stringify(slug)} is reserved per SPEC §6.1`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseVersion(s: string): [number, number] {
|
|
113
|
+
const m = VERSION_RE.exec(s);
|
|
114
|
+
const major = m ? Number(m[1]) : NaN;
|
|
115
|
+
const minor = m ? Number(m[2]) : NaN;
|
|
116
|
+
if (!(major <= U32_MAX && minor <= U32_MAX)) {
|
|
117
|
+
throw new TypeUriError("version", s, `version ${JSON.stringify(s)} must be MAJOR.MINOR per SPEC §5.1`);
|
|
118
|
+
}
|
|
119
|
+
return [major, minor];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse a Type URI in any SPEC §6.1 form.
|
|
124
|
+
*
|
|
125
|
+
* @throws {TypeUriError} if `typeUri` is not one — its `kind` says why.
|
|
126
|
+
*/
|
|
127
|
+
export function parseTypeUri(typeUri: string): TypeUri {
|
|
128
|
+
if (typeof typeUri !== "string") {
|
|
129
|
+
throw new TypeUriError("scheme", String(typeUri), "a Type URI must be a string");
|
|
130
|
+
}
|
|
131
|
+
const hash = typeUri.indexOf("#");
|
|
132
|
+
const bare = hash < 0 ? typeUri : typeUri.slice(0, hash);
|
|
133
|
+
const fragment = hash < 0 ? null : typeUri.slice(hash + 1);
|
|
134
|
+
let variant: TypeUriVariant | null = null;
|
|
135
|
+
if (fragment !== null) {
|
|
136
|
+
if (fragment !== "request" && fragment !== "response") {
|
|
137
|
+
throw new TypeUriError(
|
|
138
|
+
"fragment",
|
|
139
|
+
fragment,
|
|
140
|
+
`only #request and #response fragments are permitted in a Type URI (got #${fragment})`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
variant = fragment;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// SPEC §6.1: any absolute URI, but never `http` — an `http` Type URI would
|
|
147
|
+
// normalise a transport-downgrade path for a consumer that dereferences it.
|
|
148
|
+
const colon = bare.indexOf(":");
|
|
149
|
+
const scheme = colon < 0 ? "" : bare.slice(0, colon);
|
|
150
|
+
if (!SCHEME_RE.test(scheme) || scheme.toLowerCase() === "http") {
|
|
151
|
+
throw new TypeUriError(
|
|
152
|
+
"scheme",
|
|
153
|
+
typeUri,
|
|
154
|
+
`Type URI must have a scheme other than http (SPEC §6.1): ${JSON.stringify(typeUri)}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const shape = (): TypeUriError =>
|
|
158
|
+
new TypeUriError(
|
|
159
|
+
"shape",
|
|
160
|
+
typeUri,
|
|
161
|
+
`Type URI must be <prefix>/spec/<slug>/<major.minor>, or ` +
|
|
162
|
+
`urn:<nid>:<prefix>:spec:<slug-segments>:<major.minor>: ${JSON.stringify(typeUri)}`,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
let authority: string;
|
|
166
|
+
let slug: string;
|
|
167
|
+
let version: string;
|
|
168
|
+
let form: TypeUri["form"];
|
|
169
|
+
if (scheme.toLowerCase() === "urn") {
|
|
170
|
+
// `urn:<NID>:<prefix>:spec:<seg>:…:<MAJOR.MINOR>` — the slug is the
|
|
171
|
+
// segments between the first `:spec:` and the version.
|
|
172
|
+
const at = bare.indexOf(":spec:");
|
|
173
|
+
if (at < 0) throw shape();
|
|
174
|
+
authority = bare.slice(0, at);
|
|
175
|
+
const rest = bare.slice(at + ":spec:".length);
|
|
176
|
+
const parts = authority.split(":");
|
|
177
|
+
const nid = parts[1];
|
|
178
|
+
const ns = parts.slice(2).join(":");
|
|
179
|
+
if (!nid || !ns || rest.includes("/")) throw shape();
|
|
180
|
+
const cut = rest.lastIndexOf(":");
|
|
181
|
+
if (cut < 0) throw shape();
|
|
182
|
+
slug = rest.slice(0, cut).replace(/:/g, "/");
|
|
183
|
+
version = rest.slice(cut + 1);
|
|
184
|
+
form = "urn";
|
|
185
|
+
} else {
|
|
186
|
+
// `<prefix>/spec/<slug>/<MAJOR.MINOR>` — an https authority (with an
|
|
187
|
+
// optional path prefix), a DID URL, or any other scheme's prefix.
|
|
188
|
+
const at = bare.indexOf("/spec/");
|
|
189
|
+
if (at < 0) throw shape();
|
|
190
|
+
authority = bare.slice(0, at);
|
|
191
|
+
const afterScheme = authority.slice(scheme.length + 1);
|
|
192
|
+
if (
|
|
193
|
+
afterScheme.replace(/^\/+/, "").length === 0 ||
|
|
194
|
+
(scheme.toLowerCase() === "https" && !afterScheme.startsWith("//"))
|
|
195
|
+
) {
|
|
196
|
+
throw shape();
|
|
197
|
+
}
|
|
198
|
+
const rest = bare.slice(at + "/spec/".length);
|
|
199
|
+
// The version is the final '/' segment; everything before it is the slug.
|
|
200
|
+
const cut = rest.lastIndexOf("/");
|
|
201
|
+
if (cut < 0) throw shape();
|
|
202
|
+
slug = rest.slice(0, cut);
|
|
203
|
+
version = rest.slice(cut + 1);
|
|
204
|
+
form = "path";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
checkSlug(slug);
|
|
208
|
+
const [major, minor] = parseVersion(version);
|
|
209
|
+
return { authority, form, slug, major, minor, variant };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** {@link parseTypeUri}, returning `null` instead of throwing. */
|
|
213
|
+
export function tryParseTypeUri(typeUri: string): TypeUri | null {
|
|
214
|
+
try {
|
|
215
|
+
return parseTypeUri(typeUri);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (e instanceof TypeUriError) return null;
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The string form of `uri`. For a value {@link parseTypeUri} returned, exactly
|
|
224
|
+
* the string it parsed.
|
|
225
|
+
*/
|
|
226
|
+
export function formatTypeUri(uri: TypeUri): string {
|
|
227
|
+
const v = `${uri.major}.${uri.minor}`;
|
|
228
|
+
const base =
|
|
229
|
+
uri.form === "urn"
|
|
230
|
+
? `${uri.authority}:spec:${uri.slug.replace(/\//g, ":")}:${v}`
|
|
231
|
+
: `${uri.authority}/spec/${uri.slug}/${v}`;
|
|
232
|
+
return uri.variant === null ? base : `${base}#${uri.variant}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A sibling of `uri` at `major.minor`: the same authority, form, slug and
|
|
237
|
+
* variant, only the version changed. A declared-extension chain (SPEC §5.2)
|
|
238
|
+
* names only versions, so this is how it is walked — and why a chain never
|
|
239
|
+
* crosses authorities. (Rust's `TypeUri::with_version`.)
|
|
240
|
+
*/
|
|
241
|
+
export function typeUriWithVersion(uri: TypeUri, major: number, minor: number): TypeUri {
|
|
242
|
+
return { ...uri, major, minor };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Whether `a` and `b` name versions of the same specification: the same
|
|
247
|
+
* authority, form and slug. Two Type URIs with the same slug under different
|
|
248
|
+
* authorities are different specifications (SPEC §6.1).
|
|
249
|
+
*/
|
|
250
|
+
export function sameSpecification(a: TypeUri, b: TypeUri): boolean {
|
|
251
|
+
return a.authority === b.authority && a.form === b.form && a.slug === b.slug;
|
|
252
|
+
}
|