@interop/was-client 0.99.1 → 0.100.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 +127 -69
- package/dist/Collection.d.ts +17 -9
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +85 -70
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +11 -4
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +34 -28
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts +20 -5
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +69 -65
- package/dist/Space.js.map +1 -1
- package/dist/WasClient.d.ts +185 -57
- package/dist/WasClient.d.ts.map +1 -1
- package/dist/WasClient.js +340 -140
- package/dist/WasClient.js.map +1 -1
- package/dist/codec.d.ts +3 -2
- package/dist/codec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +1 -1
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts +7 -4
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +39 -20
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/edv/acquire.d.ts +3 -3
- package/dist/edv/acquire.d.ts.map +1 -1
- package/dist/edv/acquire.js +3 -3
- package/dist/edv/acquire.js.map +1 -1
- package/dist/edv/cipher.d.ts +1 -1
- package/dist/edv/cipher.js +1 -1
- package/dist/edv/docCipher.d.ts +6 -5
- package/dist/edv/docCipher.d.ts.map +1 -1
- package/dist/edv/docCipher.js +6 -5
- package/dist/edv/docCipher.js.map +1 -1
- package/dist/edv/encryption.d.ts +2 -2
- package/dist/edv/encryption.d.ts.map +1 -1
- package/dist/edv/encryption.js +8 -6
- package/dist/edv/encryption.js.map +1 -1
- package/dist/edv/refreshingDocCipher.d.ts +3 -3
- package/dist/edv/refreshingDocCipher.d.ts.map +1 -1
- package/dist/edv/refreshingDocCipher.js +3 -3
- package/dist/edv/refreshingDocCipher.js.map +1 -1
- package/dist/edv/transportFactory.d.ts +3 -3
- package/dist/edv/transportFactory.d.ts.map +1 -1
- package/dist/edv/transportFactory.js +3 -3
- package/dist/edv/transportFactory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codec.d.ts +7 -7
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +19 -18
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/conditional.d.ts +3 -3
- package/dist/internal/conditional.d.ts.map +1 -1
- package/dist/internal/conditional.js +5 -5
- package/dist/internal/conditional.js.map +1 -1
- package/dist/internal/grant.d.ts +15 -26
- package/dist/internal/grant.d.ts.map +1 -1
- package/dist/internal/grant.js +95 -29
- package/dist/internal/grant.js.map +1 -1
- package/dist/internal/meta.d.ts +8 -8
- package/dist/internal/meta.d.ts.map +1 -1
- package/dist/internal/meta.js +13 -10
- package/dist/internal/meta.js.map +1 -1
- package/dist/internal/paths.d.ts +423 -195
- package/dist/internal/paths.d.ts.map +1 -1
- package/dist/internal/paths.js +582 -369
- package/dist/internal/paths.js.map +1 -1
- package/dist/internal/policy.d.ts +23 -23
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +20 -20
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/request.d.ts +17 -21
- package/dist/internal/request.d.ts.map +1 -1
- package/dist/internal/request.js +10 -21
- package/dist/internal/request.js.map +1 -1
- package/dist/internal/reserved.d.ts +15 -3
- package/dist/internal/reserved.d.ts.map +1 -1
- package/dist/internal/reserved.js +37 -8
- package/dist/internal/reserved.js.map +1 -1
- package/dist/internal/revoke.d.ts +34 -13
- package/dist/internal/revoke.d.ts.map +1 -1
- package/dist/internal/revoke.js +97 -28
- package/dist/internal/revoke.js.map +1 -1
- package/dist/internal/service.d.ts +27 -7
- package/dist/internal/service.d.ts.map +1 -1
- package/dist/internal/service.js +51 -45
- package/dist/internal/service.js.map +1 -1
- package/dist/internal/write.d.ts +18 -18
- package/dist/internal/write.d.ts.map +1 -1
- package/dist/internal/write.js +27 -27
- package/dist/internal/write.js.map +1 -1
- package/dist/paths.d.ts +32 -25
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +32 -25
- package/dist/paths.js.map +1 -1
- package/dist/sync/port.d.ts +3 -3
- package/dist/sync/port.d.ts.map +1 -1
- package/dist/sync/port.js +12 -12
- package/dist/sync/port.js.map +1 -1
- package/dist/sync/provisioning.d.ts +10 -9
- package/dist/sync/provisioning.d.ts.map +1 -1
- package/dist/sync/provisioning.js +19 -18
- package/dist/sync/provisioning.js.map +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/types.d.ts +13 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/internal/paths.js
CHANGED
|
@@ -2,36 +2,41 @@
|
|
|
2
2
|
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
* URL
|
|
6
|
-
* canonicalization once.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* URL builders for the two layers the spec fixes, encoding the trailing-slash
|
|
6
|
+
* canonicalization once. Above a Space, the spec's Server Layout places the
|
|
7
|
+
* three server-level endpoints under one base server URL:
|
|
8
|
+
* `{base}service`, `{base}spaces/` and `{base}space/{space_id}/`
|
|
9
|
+
* (`serviceDescriptionUrl`, `spacesRepositoryUrl`, `spaceUrlById`, with
|
|
10
|
+
* `baseServerUrlOf` as their inverse). Beneath a Space, every URL derives from
|
|
11
|
+
* the Space URL, which is either built that way or handed out by the server
|
|
12
|
+
* (the `Location` of the create response, a listing item, a linkset, the
|
|
13
|
+
* caller's stored pointer) and used as given. A single fixed Space has no
|
|
14
|
+
* base server URL and may sit at an origin root, so a Space URL need not have
|
|
15
|
+
* a path.
|
|
16
|
+
*
|
|
17
|
+
* The convention: a trailing slash marks a container in canonical form (the
|
|
18
|
+
* Space URL itself, and `{spaceUrl}{collectionId}/`), everything else has
|
|
19
|
+
* none, and no two URLs differ only by a trailing slash. Path segments are
|
|
20
|
+
* percent-encoded so ids never break out of their slot.
|
|
11
21
|
*
|
|
12
22
|
* A container's own description is not at its URL: it lives at the reserved
|
|
13
|
-
* `meta` segment (`
|
|
14
|
-
* `
|
|
15
|
-
* 308-redirects, and a signed invocation covers the target it was
|
|
16
|
-
* so no builder here emits the bare form -- a redirect would have
|
|
17
|
-
* again.
|
|
18
|
-
*
|
|
19
|
-
* The parser is deliberately laxer than the builders. `
|
|
20
|
-
* the bare form too, because the targets it classifies are
|
|
21
|
-
* own URLs: they are the `invocationTarget` strings of
|
|
22
|
-
* a third party minted, and those name containers in
|
|
23
|
-
* as the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* `meta` segment (`spaceMetaUrl`, `collectionMetaUrl`), beside the
|
|
24
|
+
* Resource-level `resourceMetaUrl`. The bare (slash-less) form of a container
|
|
25
|
+
* URL only 308-redirects, and a signed invocation covers the target it was
|
|
26
|
+
* signed for, so no builder here emits the bare form -- a redirect would have
|
|
27
|
+
* to be signed again.
|
|
28
|
+
*
|
|
29
|
+
* The parser is deliberately laxer than the builders. `parseSpaceTarget`
|
|
30
|
+
* accepts the bare container form too, because the targets it classifies are
|
|
31
|
+
* not this client's own URLs: they are the `invocationTarget` strings of
|
|
32
|
+
* delegated capabilities a third party minted, and those name containers in
|
|
33
|
+
* either form. Reading one as the Collection or Resource it addresses is what
|
|
34
|
+
* `WasClient.fromCapability` does; refusing the bare form there would reject
|
|
35
|
+
* valid capabilities over a form this client does not control.
|
|
26
36
|
*
|
|
27
37
|
* The zcap `invocationTarget` is derived from the request URL, so these
|
|
28
38
|
* trailing-slash rules must match the server's per-operation `allowedTarget`
|
|
29
39
|
* exactly or signature verification fails.
|
|
30
|
-
*
|
|
31
|
-
* This module also owns the inverse grammar: `parseSpacePath` classifies a
|
|
32
|
-
* server pathname back into the handle depth it addresses (space / collection /
|
|
33
|
-
* resource / sub-resource), so `WasClient.fromCapability` and the builders stay
|
|
34
|
-
* in lockstep.
|
|
35
40
|
*/
|
|
36
41
|
import { ValidationError } from '../errors.js';
|
|
37
42
|
import { assertNotReserved, isReservedCollectionId, isReservedResourceId } from './reserved.js';
|
|
@@ -58,22 +63,25 @@ function encode(segment) {
|
|
|
58
63
|
}
|
|
59
64
|
/**
|
|
60
65
|
* Encodes a collection-id slot, additionally rejecting an id from the Reserved
|
|
61
|
-
* Path Segment Registry: `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Path Segment Registry: `collectionUrl` of `policy` is byte-identical to the
|
|
67
|
+
* space policy URL, so a reserved id would silently mis-target a space-level
|
|
68
|
+
* endpoint. Guarding in the builders covers every URL-forming entry point
|
|
69
|
+
* (handle constructors guard too, for the earlier failure).
|
|
65
70
|
*
|
|
66
|
-
* @param
|
|
71
|
+
* @param options {object}
|
|
72
|
+
* @param options.collectionId {string}
|
|
73
|
+
* @param options.originRoot {boolean} whether the Space is served from an
|
|
74
|
+
* origin root, where the server-level endpoint segments are reserved too
|
|
67
75
|
* @returns {string}
|
|
68
76
|
*/
|
|
69
|
-
function encodeCollectionId(collectionId) {
|
|
70
|
-
assertNotReserved({ id: collectionId, kind: 'collection' });
|
|
77
|
+
function encodeCollectionId({ collectionId, originRoot }) {
|
|
78
|
+
assertNotReserved({ id: collectionId, kind: 'collection', originRoot });
|
|
71
79
|
return encode(collectionId);
|
|
72
80
|
}
|
|
73
81
|
/**
|
|
74
82
|
* Encodes a resource-id slot, additionally rejecting an id from the Reserved
|
|
75
|
-
* Path Segment Registry: `
|
|
76
|
-
*
|
|
83
|
+
* Path Segment Registry: `resourceUrl` of `policy` is byte-identical to the
|
|
84
|
+
* collection policy URL, so a reserved id would silently mis-target a
|
|
77
85
|
* collection-level endpoint.
|
|
78
86
|
*
|
|
79
87
|
* @param resourceId {string}
|
|
@@ -84,500 +92,705 @@ function encodeResourceId(resourceId) {
|
|
|
84
92
|
return encode(resourceId);
|
|
85
93
|
}
|
|
86
94
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
95
|
+
* Parses an absolute `http(s)` URL, or returns `null`. The one rule for which
|
|
96
|
+
* URLs may name a server, a Spaces Repository, or a container here; the
|
|
97
|
+
* callers that need more (no query, a path) narrow it.
|
|
98
|
+
*
|
|
99
|
+
* @param url {string}
|
|
100
|
+
* @returns {URL | null}
|
|
101
|
+
*/
|
|
102
|
+
export function parseAbsoluteHttpUrl(url) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = new URL(url);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* {@link parseAbsoluteHttpUrl} narrowed to a URL with no query or fragment:
|
|
117
|
+
* the rule for which URLs may name a container.
|
|
118
|
+
*
|
|
119
|
+
* @param url {string}
|
|
120
|
+
* @returns {URL | null}
|
|
121
|
+
*/
|
|
122
|
+
function parseContainerUrl(url) {
|
|
123
|
+
const parsed = parseAbsoluteHttpUrl(url);
|
|
124
|
+
if (parsed === null || parsed.search !== '' || parsed.hash !== '') {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A container URL in canonical form: an absolute `http(s)` URL with no query
|
|
131
|
+
* or fragment, serialized by `URL` (lower-case host, default port dropped),
|
|
132
|
+
* ending in exactly one slash. A missing trailing slash is added, since the
|
|
133
|
+
* bare form names the same container; a run of trailing slashes is collapsed
|
|
134
|
+
* to one. An empty path segment is refused. The origin root (`https://h/`) is
|
|
135
|
+
* a valid container.
|
|
136
|
+
*
|
|
137
|
+
* @param options {object}
|
|
138
|
+
* @param options.url {string}
|
|
139
|
+
* @param options.what {string} what the URL was meant to name, for the error
|
|
140
|
+
* @returns {string}
|
|
141
|
+
* @throws {ValidationError}
|
|
142
|
+
*/
|
|
143
|
+
function canonicalContainer({ url, what }) {
|
|
144
|
+
const parsed = parseContainerUrl(url);
|
|
145
|
+
if (parsed === null) {
|
|
146
|
+
throw new ValidationError(`"${url}" is not a ${what}: an absolute http(s) URL with no query or ` +
|
|
147
|
+
'fragment is required.');
|
|
148
|
+
}
|
|
149
|
+
const pathname = parsed.pathname.replace(/\/+$/, '');
|
|
150
|
+
if (pathname.split('/').slice(1).includes('')) {
|
|
151
|
+
throw new ValidationError(`"${url}" is not a ${what}: its path has an empty segment.`);
|
|
152
|
+
}
|
|
153
|
+
return `${parsed.origin}${pathname}/`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a canonical container URL is an origin root (`https://h/`).
|
|
157
|
+
*
|
|
158
|
+
* @param canonical {string}
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
export function isOriginRoot(canonical) {
|
|
162
|
+
// Canonical form is `{origin}{path}/`, so the URL is a root when the first
|
|
163
|
+
// slash after the scheme's `//` is its last character.
|
|
164
|
+
const pathStart = canonical.indexOf('/', canonical.indexOf('//') + 2);
|
|
165
|
+
return pathStart === canonical.length - 1;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The base server URL in canonical form: the URL under which a server lays
|
|
169
|
+
* out `service`, `spaces/` and `space/{space_id}/`. An absolute `http(s)` URL
|
|
170
|
+
* with no query or fragment; a missing trailing slash is supplied rather than
|
|
171
|
+
* refused, since `https://h/pws` and `https://h/pws/` can only mean the same
|
|
172
|
+
* mount.
|
|
92
173
|
*
|
|
93
|
-
* @param
|
|
174
|
+
* @param baseServerUrl {string}
|
|
94
175
|
* @returns {string}
|
|
176
|
+
* @throws {ValidationError} when the value is not an absolute `http(s)` URL,
|
|
177
|
+
* or carries a query or fragment
|
|
95
178
|
*/
|
|
96
|
-
function
|
|
97
|
-
return
|
|
179
|
+
export function canonicalBaseServerUrl(baseServerUrl) {
|
|
180
|
+
return canonicalContainer({ url: baseServerUrl, what: 'base server URL' });
|
|
98
181
|
}
|
|
99
182
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Collections, whose `POST` creates one, and whose `DELETE` removes the Space.
|
|
103
|
-
* The Space is an ordinary container, so its members are listed and created at
|
|
104
|
-
* the container itself -- there is no second builder for that.
|
|
183
|
+
* `{base}service` -- the service description of the server at
|
|
184
|
+
* `baseServerUrl`.
|
|
105
185
|
*
|
|
106
|
-
* @param
|
|
186
|
+
* @param options {object}
|
|
187
|
+
* @param options.baseServerUrl {string}
|
|
107
188
|
* @returns {string}
|
|
108
189
|
*/
|
|
109
|
-
export function
|
|
110
|
-
return `${
|
|
190
|
+
export function serviceDescriptionUrl({ baseServerUrl }) {
|
|
191
|
+
return `${canonicalBaseServerUrl(baseServerUrl)}service`;
|
|
111
192
|
}
|
|
112
193
|
/**
|
|
113
|
-
*
|
|
194
|
+
* `{base}spaces/` -- the Spaces Repository of the server at `baseServerUrl`,
|
|
195
|
+
* in canonical container form. The server may not implement one; its service
|
|
196
|
+
* description says.
|
|
197
|
+
*
|
|
198
|
+
* @param options {object}
|
|
199
|
+
* @param options.baseServerUrl {string}
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
export function spacesRepositoryUrl({ baseServerUrl }) {
|
|
203
|
+
return `${canonicalBaseServerUrl(baseServerUrl)}spaces/`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* `{base}space/{space_id}/` -- the Space with id `spaceId` on the server at
|
|
207
|
+
* `baseServerUrl`, in canonical form. The one place a Space URL is built
|
|
208
|
+
* rather than received; the spec's Server Layout fixes this shape.
|
|
209
|
+
*
|
|
210
|
+
* @param options {object}
|
|
211
|
+
* @param options.baseServerUrl {string}
|
|
212
|
+
* @param options.spaceId {string}
|
|
213
|
+
* @returns {string}
|
|
214
|
+
* @throws {ValidationError} when `spaceId` is empty or a dot segment
|
|
215
|
+
*/
|
|
216
|
+
export function spaceUrlById({ baseServerUrl, spaceId }) {
|
|
217
|
+
return `${canonicalBaseServerUrl(baseServerUrl)}space/${encode(spaceId)}/`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The base server URL a server-level URL sits under: the inverse of
|
|
221
|
+
* {@link serviceDescriptionUrl}, {@link spacesRepositoryUrl} and
|
|
222
|
+
* {@link spaceUrlById}. Accepts the service description URL (`{base}service`),
|
|
223
|
+
* the Spaces Repository URL (`{base}spaces/`, slash optional) or a Space URL
|
|
224
|
+
* (`{base}space/{space_id}/`, slash optional) and strips exactly the segments
|
|
225
|
+
* the layout adds. Anything else is refused, including a Collection or
|
|
226
|
+
* Resource URL, whose depth cannot be told from the string alone, and the
|
|
227
|
+
* URL of a single fixed Space, which has no base server URL.
|
|
228
|
+
*
|
|
229
|
+
* @param url {string}
|
|
230
|
+
* @returns {string}
|
|
231
|
+
* @throws {ValidationError} when `url` is not an absolute `http(s)` URL
|
|
232
|
+
* without query or fragment in one of the three forms
|
|
233
|
+
*/
|
|
234
|
+
export function baseServerUrlOf(url) {
|
|
235
|
+
const parsed = parseContainerUrl(url);
|
|
236
|
+
const segments = parsed?.pathname.split('/').filter(Boolean) ?? [];
|
|
237
|
+
const last = segments.at(-1);
|
|
238
|
+
// The Space form is tested first: a Space whose id is itself `spaces` or
|
|
239
|
+
// `service` still sits under `space/`, and the other two forms have no
|
|
240
|
+
// `space` segment before their last one.
|
|
241
|
+
const isSpace = segments.at(-2) === 'space';
|
|
242
|
+
const isServerLevel = isSpace ||
|
|
243
|
+
last === 'spaces' ||
|
|
244
|
+
(last === 'service' && !parsed?.pathname.endsWith('/'));
|
|
245
|
+
if (parsed === null || !isServerLevel) {
|
|
246
|
+
throw new ValidationError(`"${url}" is not a server-level PWS URL: expected the service ` +
|
|
247
|
+
'description (`{base}service`), the Spaces Repository ' +
|
|
248
|
+
'(`{base}spaces/`) or a Space (`{base}space/{space_id}/`).');
|
|
249
|
+
}
|
|
250
|
+
const base = segments.slice(0, isSpace ? -2 : -1);
|
|
251
|
+
return `${parsed.origin}/${base.map(segment => `${segment}/`).join('')}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Splits an absolute Collection URL into the Space URL it sits under and the
|
|
255
|
+
* Collection id: the parent container and the last segment, percent-decoded.
|
|
256
|
+
* A Collection URL is always the Space URL plus one segment, so no layout
|
|
257
|
+
* knowledge is needed. The bare form (no trailing slash) is accepted, as for
|
|
258
|
+
* any container URL received from outside.
|
|
259
|
+
*
|
|
260
|
+
* @param collectionUrl {string}
|
|
261
|
+
* @returns {{ spaceUrl: string; collectionId: string }}
|
|
262
|
+
* @throws {ValidationError} when `collectionUrl` is not an absolute `http(s)`
|
|
263
|
+
* URL without query or fragment, has no path segment, or its last segment
|
|
264
|
+
* is a malformed percent escape
|
|
265
|
+
*/
|
|
266
|
+
export function splitCollectionUrl(collectionUrl) {
|
|
267
|
+
const canonical = canonicalContainer({
|
|
268
|
+
url: collectionUrl,
|
|
269
|
+
what: 'Collection URL'
|
|
270
|
+
});
|
|
271
|
+
if (isOriginRoot(canonical)) {
|
|
272
|
+
throw new ValidationError(`"${collectionUrl}" is not a Collection URL: a Collection has at least ` +
|
|
273
|
+
'one path segment beneath its origin.');
|
|
274
|
+
}
|
|
275
|
+
const parsed = new URL(canonical);
|
|
276
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
277
|
+
const encodedId = segments.pop();
|
|
278
|
+
let collectionId;
|
|
279
|
+
try {
|
|
280
|
+
collectionId = decodeURIComponent(encodedId);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
throw new ValidationError(`"${collectionUrl}" is not a Collection URL: its last segment is not ` +
|
|
284
|
+
'a well-formed percent-encoding.', { cause: err });
|
|
285
|
+
}
|
|
286
|
+
const parent = segments.length === 0 ? '' : `${segments.join('/')}/`;
|
|
287
|
+
return { spaceUrl: `${parsed.origin}/${parent}`, collectionId };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The Space URL in canonical form (see `canonicalContainer`). A server names
|
|
291
|
+
* a Space in this form (the `Location` of the create response, a listing
|
|
292
|
+
* item's `url`), and every URL beneath the Space, and every zcap
|
|
293
|
+
* `invocationTarget`, is derived from it, so a caller's own serialization of
|
|
294
|
+
* the same Space must reduce to the same string. An origin root is a valid
|
|
295
|
+
* Space URL: a single fixed Space (one with no Space id and no base server
|
|
296
|
+
* URL) may be served there.
|
|
297
|
+
*
|
|
298
|
+
* @param spaceUrl {string} a Space URL, as the server named it or as the
|
|
299
|
+
* caller stored it
|
|
300
|
+
* @returns {string}
|
|
301
|
+
* @throws {ValidationError} when `spaceUrl` is not an absolute `http(s)`
|
|
302
|
+
* URL, carries a query or fragment, or has an empty path segment
|
|
303
|
+
*/
|
|
304
|
+
export function canonicalSpaceUrl(spaceUrl) {
|
|
305
|
+
const canonical = canonicalContainer({ url: spaceUrl, what: 'Space URL' });
|
|
306
|
+
// The last segment is the Space id, percent-encoded; a malformed escape
|
|
307
|
+
// (`%ff`) is refused here so every handle beneath the Space (not only the
|
|
308
|
+
// Space handle, which decodes the id) rejects an unparseable Space URL.
|
|
309
|
+
spaceIdOfCanonicalUrl(canonical);
|
|
310
|
+
return canonical;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* The Space's id as its URL names it: the last path segment of the Space
|
|
314
|
+
* URL, percent-decoded. The spec's hierarchy and its did:webvh profile both
|
|
315
|
+
* place the id there (`.../space/{space_id}/`), and it is the value the Space
|
|
316
|
+
* Metadata object's `id` carries. A single fixed Space at an origin root has
|
|
317
|
+
* no id, so that URL is refused here.
|
|
318
|
+
*
|
|
319
|
+
* @param spaceUrl {string} a Space URL
|
|
320
|
+
* @returns {string}
|
|
321
|
+
* @throws {ValidationError} when `spaceUrl` is not a Space URL (see
|
|
322
|
+
* {@link canonicalSpaceUrl}), is an origin root, or its last segment is a
|
|
323
|
+
* malformed percent escape
|
|
324
|
+
*/
|
|
325
|
+
export function spaceIdOfUrl(spaceUrl) {
|
|
326
|
+
const id = spaceIdOfCanonicalUrl(canonicalSpaceUrl(spaceUrl));
|
|
327
|
+
if (id === undefined) {
|
|
328
|
+
throw new ValidationError(`"${spaceUrl}" names no Space id: it is an origin root, which only a ` +
|
|
329
|
+
'single fixed Space (one without an id) is served from.');
|
|
330
|
+
}
|
|
331
|
+
return id;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* {@link spaceIdOfUrl} for a Space URL already in canonical form (a handle's
|
|
335
|
+
* `url`, the result of {@link canonicalSpaceUrl}), sparing the second parse.
|
|
336
|
+
* Returns `undefined` for an origin root, which has no id segment.
|
|
337
|
+
*
|
|
338
|
+
* @param canonical {string} a canonical Space URL
|
|
339
|
+
* @returns {string | undefined}
|
|
340
|
+
* @throws {ValidationError} when the last segment is a malformed percent
|
|
341
|
+
* escape
|
|
342
|
+
*/
|
|
343
|
+
export function spaceIdOfCanonicalUrl(canonical) {
|
|
344
|
+
if (isOriginRoot(canonical)) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const segment = canonical.split('/').filter(Boolean).pop();
|
|
348
|
+
try {
|
|
349
|
+
return decodeURIComponent(segment);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
throw new ValidationError(`"${canonical}" is not a Space URL: its last segment is not a ` +
|
|
353
|
+
'well-formed percent-encoded id.', { cause: err });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* `{spaceUrl}meta` -- the Space Metadata object: the Space's description
|
|
114
358
|
* (`name`, `controller`, `type`), read with `GET` and replaced with `PUT`.
|
|
359
|
+
* The Space container itself (the canonical Space URL) is the target a Space
|
|
360
|
+
* root capability names, the URL whose `GET` lists the Space's Collections,
|
|
361
|
+
* whose `POST` creates one, and whose `DELETE` removes the Space; it has no
|
|
362
|
+
* builder, since the caller holds it.
|
|
115
363
|
*
|
|
116
|
-
* @param
|
|
364
|
+
* @param options {object}
|
|
365
|
+
* @param options.spaceUrl {string}
|
|
117
366
|
* @returns {string}
|
|
118
367
|
*/
|
|
119
|
-
export function
|
|
120
|
-
return `${
|
|
368
|
+
export function spaceMetaUrl({ spaceUrl }) {
|
|
369
|
+
return `${canonicalSpaceUrl(spaceUrl)}meta`;
|
|
121
370
|
}
|
|
122
371
|
/**
|
|
123
|
-
*
|
|
372
|
+
* `{spaceUrl}export` -- export a space as a tar archive.
|
|
373
|
+
*
|
|
374
|
+
* @param options {object}
|
|
375
|
+
* @param options.spaceUrl {string}
|
|
376
|
+
* @returns {string}
|
|
124
377
|
*/
|
|
125
|
-
export function
|
|
126
|
-
return `${
|
|
378
|
+
export function spaceExportUrl({ spaceUrl }) {
|
|
379
|
+
return `${canonicalSpaceUrl(spaceUrl)}export`;
|
|
127
380
|
}
|
|
128
381
|
/**
|
|
129
|
-
*
|
|
382
|
+
* `{spaceUrl}import` -- import a tar archive into a space.
|
|
383
|
+
*
|
|
384
|
+
* @param options {object}
|
|
385
|
+
* @param options.spaceUrl {string}
|
|
386
|
+
* @returns {string}
|
|
130
387
|
*/
|
|
131
|
-
export function
|
|
132
|
-
return `${
|
|
388
|
+
export function spaceImportUrl({ spaceUrl }) {
|
|
389
|
+
return `${canonicalSpaceUrl(spaceUrl)}import`;
|
|
133
390
|
}
|
|
134
391
|
/**
|
|
135
|
-
*
|
|
392
|
+
* `{spaceUrl}backends` -- the backends available within a space (list, and
|
|
136
393
|
* register a new `external` backend).
|
|
394
|
+
*
|
|
395
|
+
* @param options {object}
|
|
396
|
+
* @param options.spaceUrl {string}
|
|
397
|
+
* @returns {string}
|
|
137
398
|
*/
|
|
138
|
-
export function
|
|
139
|
-
return `${
|
|
399
|
+
export function spaceBackendsUrl({ spaceUrl }) {
|
|
400
|
+
return `${canonicalSpaceUrl(spaceUrl)}backends`;
|
|
140
401
|
}
|
|
141
402
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
403
|
+
* `{spaceUrl}backends/{backendId}` -- a single registered `external` backend
|
|
404
|
+
* (replace / deregister by id).
|
|
405
|
+
*
|
|
406
|
+
* @param options {object}
|
|
407
|
+
* @param options.spaceUrl {string}
|
|
408
|
+
* @param options.backendId {string}
|
|
409
|
+
* @returns {string}
|
|
144
410
|
*/
|
|
145
|
-
export function
|
|
146
|
-
return `${
|
|
411
|
+
export function registeredBackendUrl({ spaceUrl, backendId }) {
|
|
412
|
+
return `${spaceBackendsUrl({ spaceUrl })}/${encode(backendId)}`;
|
|
147
413
|
}
|
|
148
414
|
/**
|
|
149
|
-
*
|
|
415
|
+
* `{spaceUrl}quotas` -- the space-level storage quota report.
|
|
416
|
+
*
|
|
417
|
+
* @param options {object}
|
|
418
|
+
* @param options.spaceUrl {string}
|
|
419
|
+
* @returns {string}
|
|
150
420
|
*/
|
|
151
|
-
export function
|
|
152
|
-
return `${
|
|
421
|
+
export function spaceQuotasUrl({ spaceUrl }) {
|
|
422
|
+
return `${canonicalSpaceUrl(spaceUrl)}quotas`;
|
|
153
423
|
}
|
|
154
424
|
/**
|
|
155
|
-
*
|
|
425
|
+
* `{spaceUrl}policy` -- the space-level access-control policy resource.
|
|
426
|
+
*
|
|
427
|
+
* @param options {object}
|
|
428
|
+
* @param options.spaceUrl {string}
|
|
429
|
+
* @returns {string}
|
|
156
430
|
*/
|
|
157
|
-
export function
|
|
158
|
-
return `${
|
|
431
|
+
export function spacePolicyUrl({ spaceUrl }) {
|
|
432
|
+
return `${canonicalSpaceUrl(spaceUrl)}policy`;
|
|
159
433
|
}
|
|
160
434
|
/**
|
|
161
|
-
*
|
|
435
|
+
* `{spaceUrl}linkset` -- the space-level linkset (policy discovery).
|
|
436
|
+
*
|
|
437
|
+
* @param options {object}
|
|
438
|
+
* @param options.spaceUrl {string}
|
|
439
|
+
* @returns {string}
|
|
162
440
|
*/
|
|
163
|
-
export function
|
|
164
|
-
return `${
|
|
441
|
+
export function spaceLinksetUrl({ spaceUrl }) {
|
|
442
|
+
return `${canonicalSpaceUrl(spaceUrl)}linkset`;
|
|
165
443
|
}
|
|
166
444
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
445
|
+
* `{spaceUrl}replicas` -- the Space's replica registrations: `GET` lists them
|
|
446
|
+
* and `POST` registers one. Controller-only, both forms.
|
|
447
|
+
*
|
|
448
|
+
* @param options {object}
|
|
449
|
+
* @param options.spaceUrl {string}
|
|
450
|
+
* @returns {string}
|
|
169
451
|
*/
|
|
170
|
-
export function
|
|
171
|
-
return `${
|
|
452
|
+
export function spaceReplicasUrl({ spaceUrl }) {
|
|
453
|
+
return `${canonicalSpaceUrl(spaceUrl)}replicas`;
|
|
172
454
|
}
|
|
173
455
|
/**
|
|
174
|
-
*
|
|
456
|
+
* `{spaceUrl}replicas/{replicaId}` -- one replica registration (read and
|
|
175
457
|
* delete by id).
|
|
458
|
+
*
|
|
459
|
+
* @param options {object}
|
|
460
|
+
* @param options.spaceUrl {string}
|
|
461
|
+
* @param options.replicaId {string}
|
|
462
|
+
* @returns {string}
|
|
176
463
|
*/
|
|
177
|
-
export function
|
|
178
|
-
return `${
|
|
464
|
+
export function replicaRegistrationUrl({ spaceUrl, replicaId }) {
|
|
465
|
+
return `${spaceReplicasUrl({ spaceUrl })}/${encode(replicaId)}`;
|
|
179
466
|
}
|
|
180
467
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
468
|
+
* `{spaceUrl}replicas/{replicaId}/status` -- the pull loop's runtime state
|
|
469
|
+
* for one registration.
|
|
470
|
+
*
|
|
471
|
+
* @param options {object}
|
|
472
|
+
* @param options.spaceUrl {string}
|
|
473
|
+
* @param options.replicaId {string}
|
|
474
|
+
* @returns {string}
|
|
183
475
|
*/
|
|
184
|
-
export function
|
|
185
|
-
return `${
|
|
476
|
+
export function replicaStatusUrl({ spaceUrl, replicaId }) {
|
|
477
|
+
return `${replicaRegistrationUrl({ spaceUrl, replicaId })}/status`;
|
|
186
478
|
}
|
|
187
479
|
/**
|
|
188
|
-
*
|
|
480
|
+
* `{spaceUrl}zcaps/revocations/{capabilityId}` -- submit a revocation of a
|
|
189
481
|
* Space-rooted capability. The capability's `id` (typically a `urn:uuid:`) is
|
|
190
482
|
* percent-encoded into the single final segment. `zcaps` is in the Reserved
|
|
191
483
|
* Path Segment Registry, so no Collection can take that slot.
|
|
484
|
+
*
|
|
485
|
+
* @param options {object}
|
|
486
|
+
* @param options.spaceUrl {string}
|
|
487
|
+
* @param options.capabilityId {string}
|
|
488
|
+
* @returns {string}
|
|
192
489
|
*/
|
|
193
|
-
export function
|
|
194
|
-
return `${
|
|
490
|
+
export function spaceRevocationUrl({ spaceUrl, capabilityId }) {
|
|
491
|
+
return `${canonicalSpaceUrl(spaceUrl)}zcaps/revocations/${encode(capabilityId)}`;
|
|
195
492
|
}
|
|
196
493
|
/**
|
|
197
|
-
*
|
|
198
|
-
* sub-endpoints and the Resource ids hang off. Module-private
|
|
199
|
-
*
|
|
494
|
+
* `{spaceUrl}{collectionId}` -- the slash-less prefix the collection-level
|
|
495
|
+
* sub-endpoints and the Resource ids hang off. Module-private: the bare form
|
|
496
|
+
* is not a URL this client ever requests (the server only 308-redirects it),
|
|
497
|
+
* so it is a prefix here and never a builder's return value.
|
|
200
498
|
*
|
|
201
|
-
* @param
|
|
202
|
-
* @param
|
|
499
|
+
* @param options {object}
|
|
500
|
+
* @param options.spaceUrl {string}
|
|
501
|
+
* @param options.collectionId {string}
|
|
203
502
|
* @returns {string}
|
|
204
503
|
*/
|
|
205
|
-
function collectionPrefix(
|
|
206
|
-
|
|
504
|
+
function collectionPrefix({ spaceUrl, collectionId }) {
|
|
505
|
+
const canonical = canonicalSpaceUrl(spaceUrl);
|
|
506
|
+
// At an origin root the Collection id is the first path segment, where a
|
|
507
|
+
// server with a base server URL places its three server-level endpoints,
|
|
508
|
+
// so the spec reserves those segments there (and only there).
|
|
509
|
+
const encodedId = encodeCollectionId({
|
|
510
|
+
collectionId,
|
|
511
|
+
originRoot: isOriginRoot(canonical)
|
|
512
|
+
});
|
|
513
|
+
return `${canonical}${encodedId}`;
|
|
207
514
|
}
|
|
208
515
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
516
|
+
* `{spaceUrl}{collectionId}/` -- the Collection container in canonical form:
|
|
517
|
+
* the target a "share this collection" capability names, the URL whose `GET`
|
|
518
|
+
* lists its Resources, whose `POST` adds one, and whose `DELETE` removes the
|
|
519
|
+
* Collection.
|
|
213
520
|
*
|
|
214
|
-
* @param
|
|
215
|
-
* @param
|
|
521
|
+
* @param options {object}
|
|
522
|
+
* @param options.spaceUrl {string}
|
|
523
|
+
* @param options.collectionId {string}
|
|
216
524
|
* @returns {string}
|
|
217
525
|
*/
|
|
218
|
-
export function
|
|
219
|
-
return `${collectionPrefix(
|
|
526
|
+
export function collectionUrl({ spaceUrl, collectionId }) {
|
|
527
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/`;
|
|
220
528
|
}
|
|
221
529
|
/**
|
|
222
|
-
*
|
|
530
|
+
* `{spaceUrl}{collectionId}/policy` -- the collection-level access-control
|
|
223
531
|
* policy resource.
|
|
532
|
+
*
|
|
533
|
+
* @param options {object}
|
|
534
|
+
* @param options.spaceUrl {string}
|
|
535
|
+
* @param options.collectionId {string}
|
|
536
|
+
* @returns {string}
|
|
224
537
|
*/
|
|
225
|
-
export function
|
|
226
|
-
return `${collectionPrefix(
|
|
538
|
+
export function collectionPolicyUrl({ spaceUrl, collectionId }) {
|
|
539
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/policy`;
|
|
227
540
|
}
|
|
228
541
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
542
|
+
* `{spaceUrl}{collectionId}/linkset` -- the collection-level linkset (policy
|
|
543
|
+
* discovery).
|
|
544
|
+
*
|
|
545
|
+
* @param options {object}
|
|
546
|
+
* @param options.spaceUrl {string}
|
|
547
|
+
* @param options.collectionId {string}
|
|
548
|
+
* @returns {string}
|
|
231
549
|
*/
|
|
232
|
-
export function
|
|
233
|
-
return `${collectionPrefix(
|
|
550
|
+
export function collectionLinksetUrl({ spaceUrl, collectionId }) {
|
|
551
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/linkset`;
|
|
234
552
|
}
|
|
235
553
|
/**
|
|
236
|
-
*
|
|
554
|
+
* `{spaceUrl}{collectionId}/backend` -- the "Collection Backend Selected"
|
|
237
555
|
* descriptor (the backend this collection is stored on).
|
|
556
|
+
*
|
|
557
|
+
* @param options {object}
|
|
558
|
+
* @param options.spaceUrl {string}
|
|
559
|
+
* @param options.collectionId {string}
|
|
560
|
+
* @returns {string}
|
|
238
561
|
*/
|
|
239
|
-
export function
|
|
240
|
-
return `${collectionPrefix(
|
|
562
|
+
export function collectionBackendUrl({ spaceUrl, collectionId }) {
|
|
563
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/backend`;
|
|
241
564
|
}
|
|
242
565
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
566
|
+
* `{spaceUrl}{collectionId}/quota` -- the per-collection storage quota report
|
|
567
|
+
* (spec "Quotas").
|
|
568
|
+
*
|
|
569
|
+
* @param options {object}
|
|
570
|
+
* @param options.spaceUrl {string}
|
|
571
|
+
* @param options.collectionId {string}
|
|
572
|
+
* @returns {string}
|
|
245
573
|
*/
|
|
246
|
-
export function
|
|
247
|
-
return `${collectionPrefix(
|
|
574
|
+
export function collectionQuotaUrl({ spaceUrl, collectionId }) {
|
|
575
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/quota`;
|
|
248
576
|
}
|
|
249
577
|
/**
|
|
250
|
-
*
|
|
578
|
+
* `{spaceUrl}{collectionId}/meta` -- the Collection Metadata object: the
|
|
251
579
|
* Collection's configuration (`name`, `backend`, `encryption`, `generator`)
|
|
252
580
|
* together with the server-managed timestamps / `createdBy` and the
|
|
253
581
|
* user-writable `custom` and `epoch`, as one object under one validator.
|
|
582
|
+
*
|
|
583
|
+
* @param options {object}
|
|
584
|
+
* @param options.spaceUrl {string}
|
|
585
|
+
* @param options.collectionId {string}
|
|
586
|
+
* @returns {string}
|
|
254
587
|
*/
|
|
255
|
-
export function
|
|
256
|
-
return `${collectionPrefix(
|
|
588
|
+
export function collectionMetaUrl({ spaceUrl, collectionId }) {
|
|
589
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/meta`;
|
|
257
590
|
}
|
|
258
591
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
592
|
+
* `{spaceUrl}{collectionId}/meta/log` -- the Collection's governing history
|
|
593
|
+
* log sub-resource (JSON Lines). Not a Resource of the Collection: absent from
|
|
594
|
+
* listings and the `changes` feed, and versioned by its own ETag.
|
|
595
|
+
*
|
|
596
|
+
* @param options {object}
|
|
597
|
+
* @param options.spaceUrl {string}
|
|
598
|
+
* @param options.collectionId {string}
|
|
599
|
+
* @returns {string}
|
|
262
600
|
*/
|
|
263
|
-
export function
|
|
264
|
-
return `${
|
|
601
|
+
export function collectionLogUrl({ spaceUrl, collectionId }) {
|
|
602
|
+
return `${collectionMetaUrl({ spaceUrl, collectionId })}/log`;
|
|
265
603
|
}
|
|
266
604
|
/**
|
|
267
|
-
*
|
|
605
|
+
* `{spaceUrl}{collectionId}/query` -- the collection-level query endpoint,
|
|
268
606
|
* whose body's `profile` selects the query (e.g. `changes`, `blinded-index`).
|
|
607
|
+
*
|
|
608
|
+
* @param options {object}
|
|
609
|
+
* @param options.spaceUrl {string}
|
|
610
|
+
* @param options.collectionId {string}
|
|
611
|
+
* @returns {string}
|
|
269
612
|
*/
|
|
270
|
-
export function
|
|
271
|
-
return `${collectionPrefix(
|
|
613
|
+
export function collectionQueryUrl({ spaceUrl, collectionId }) {
|
|
614
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/query`;
|
|
272
615
|
}
|
|
273
616
|
/**
|
|
274
|
-
*
|
|
617
|
+
* `{spaceUrl}{collectionId}/{resourceId}` -- get / put / delete a resource
|
|
275
618
|
* (no trailing slash).
|
|
276
|
-
*/
|
|
277
|
-
export function resourcePath(spaceId, collectionId, resourceId) {
|
|
278
|
-
return `${collectionPrefix(spaceId, collectionId)}/${encodeResourceId(resourceId)}`;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* `/space/:spaceId/:collectionId/:resourceId/meta` -- the resource metadata
|
|
282
|
-
* object (server-managed properties plus the user-writable `custom` object).
|
|
283
|
-
*/
|
|
284
|
-
export function resourceMeta(spaceId, collectionId, resourceId) {
|
|
285
|
-
return `${resourcePath(spaceId, collectionId, resourceId)}/meta`;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* `/space/:spaceId/:collectionId/:resourceId/policy` -- the resource-level
|
|
289
|
-
* access-control policy resource.
|
|
290
|
-
*/
|
|
291
|
-
export function resourcePolicy(spaceId, collectionId, resourceId) {
|
|
292
|
-
return `${resourcePath(spaceId, collectionId, resourceId)}/policy`;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* `/space/:spaceId/:collectionId/:resourceId/chunks/:chunkIndex` -- a single
|
|
296
|
-
* stored chunk of a chunked Resource. Member
|
|
297
|
-
* form, no trailing slash: get / put / delete one chunk by its ordinal index.
|
|
298
|
-
* The `chunkIndex` is a non-negative integer, emitted verbatim (it is never a
|
|
299
|
-
* reserved or dot segment, so it needs no percent-encoding).
|
|
300
619
|
*
|
|
301
|
-
* @param
|
|
302
|
-
* @param
|
|
303
|
-
* @param
|
|
304
|
-
* @param
|
|
620
|
+
* @param options {object}
|
|
621
|
+
* @param options.spaceUrl {string}
|
|
622
|
+
* @param options.collectionId {string}
|
|
623
|
+
* @param options.resourceId {string}
|
|
305
624
|
* @returns {string}
|
|
306
625
|
*/
|
|
307
|
-
export function
|
|
308
|
-
return `${
|
|
626
|
+
export function resourceUrl({ spaceUrl, collectionId, resourceId }) {
|
|
627
|
+
return `${collectionPrefix({ spaceUrl, collectionId })}/${encodeResourceId(resourceId)}`;
|
|
309
628
|
}
|
|
310
629
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* slash (the member form, {@link resourceChunkPath}, has none).
|
|
630
|
+
* `{spaceUrl}{collectionId}/{resourceId}/meta` -- the resource metadata
|
|
631
|
+
* object (server-managed properties plus the user-writable `custom` object).
|
|
314
632
|
*
|
|
315
|
-
* @param
|
|
316
|
-
* @param
|
|
317
|
-
* @param
|
|
633
|
+
* @param options {object}
|
|
634
|
+
* @param options.spaceUrl {string}
|
|
635
|
+
* @param options.collectionId {string}
|
|
636
|
+
* @param options.resourceId {string}
|
|
318
637
|
* @returns {string}
|
|
319
638
|
*/
|
|
320
|
-
export function
|
|
321
|
-
return `${
|
|
639
|
+
export function resourceMetaUrl({ spaceUrl, collectionId, resourceId }) {
|
|
640
|
+
return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/meta`;
|
|
322
641
|
}
|
|
323
642
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* URL a `WasClient` is constructed with (its `serverUrl`), so a consumer that
|
|
327
|
-
* holds only a Space or Collection URL forms the client's `serverUrl` from the
|
|
328
|
-
* base `parseAbsoluteSpaceTarget` reads off it.
|
|
643
|
+
* `{spaceUrl}{collectionId}/{resourceId}/policy` -- the resource-level
|
|
644
|
+
* access-control policy resource.
|
|
329
645
|
*
|
|
330
646
|
* @param options {object}
|
|
331
|
-
* @param options.
|
|
647
|
+
* @param options.spaceUrl {string}
|
|
648
|
+
* @param options.collectionId {string}
|
|
649
|
+
* @param options.resourceId {string}
|
|
332
650
|
* @returns {string}
|
|
333
651
|
*/
|
|
334
|
-
export function
|
|
335
|
-
return
|
|
652
|
+
export function resourcePolicyUrl({ spaceUrl, collectionId, resourceId }) {
|
|
653
|
+
return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/policy`;
|
|
336
654
|
}
|
|
337
655
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* default port dropped), so two serializations of one URL compare equal:
|
|
344
|
-
* `https://host/was/x/...` with the index of `x` yields `https://host/was`,
|
|
345
|
-
* and `https://host/x/...` yields `https://host`.
|
|
346
|
-
*
|
|
347
|
-
* This is the one rule for which base URLs are well formed, shared by
|
|
348
|
-
* `baseUrlOfSpacesUrl` and `parseAbsoluteSpaceTarget` so a base read off a
|
|
349
|
-
* Space URL always survives `spacesUrl` and the `WasClient` constructor.
|
|
350
|
-
* Returns `null` when `url` is not an absolute `http(s)` URL, carries a query
|
|
351
|
-
* or fragment, when `boundary` finds no segment, or when a segment before it
|
|
352
|
-
* is empty (`https://host//was/x`): a doubled slash would leave the base with
|
|
353
|
-
* a trailing slash.
|
|
656
|
+
* `{spaceUrl}{collectionId}/{resourceId}/chunks/{chunkIndex}` -- a single
|
|
657
|
+
* stored chunk of a chunked Resource. Member form, no trailing slash: get /
|
|
658
|
+
* put / delete one chunk by its ordinal index. The `chunkIndex` is a
|
|
659
|
+
* non-negative integer, emitted verbatim (it is never a reserved or dot
|
|
660
|
+
* segment, so it needs no percent-encoding).
|
|
354
661
|
*
|
|
355
662
|
* @param options {object}
|
|
356
|
-
* @param options.
|
|
357
|
-
* @param options.
|
|
358
|
-
*
|
|
359
|
-
* @
|
|
360
|
-
*
|
|
663
|
+
* @param options.spaceUrl {string}
|
|
664
|
+
* @param options.collectionId {string}
|
|
665
|
+
* @param options.resourceId {string}
|
|
666
|
+
* @param options.chunkIndex {number}
|
|
667
|
+
* @returns {string}
|
|
361
668
|
*/
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
parsed = new URL(url);
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
if ((parsed.protocol !== 'https:' && parsed.protocol !== 'http:') ||
|
|
371
|
-
parsed.search !== '' ||
|
|
372
|
-
parsed.hash !== '') {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
const segments = parsed.pathname.split('/').slice(1);
|
|
376
|
-
const index = boundary(segments);
|
|
377
|
-
if (index === -1) {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
const basePath = segments.slice(0, index);
|
|
381
|
-
if (basePath.some(segment => segment === '')) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
const baseUrl = basePath.length === 0
|
|
385
|
-
? parsed.origin
|
|
386
|
-
: `${parsed.origin}/${basePath.join('/')}`;
|
|
387
|
-
return { baseUrl, rest: segments.slice(index) };
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* The server base URL a Spaces Repository URL sits beneath: everything before
|
|
391
|
-
* its final `spaces` segment, without a trailing slash, so
|
|
392
|
-
* `https://host/was/spaces/` yields `https://host/was` and
|
|
393
|
-
* `https://host/spaces/` yields `https://host`. A trailing run of slashes is
|
|
394
|
-
* collapsed first, so `https://host/spaces` and `https://host/spaces//` read
|
|
395
|
-
* the same. The result is in `URL` canonical form (lower-case host, default
|
|
396
|
-
* port dropped), so two serializations of one URL compare equal.
|
|
397
|
-
*
|
|
398
|
-
* Returns `null` when `url` is not an absolute `http(s)` URL, its last path
|
|
399
|
-
* segment is not `spaces`, it has an empty path segment (`https://host//spaces`),
|
|
400
|
-
* or it carries a query or fragment; the caller chooses the error.
|
|
401
|
-
*
|
|
402
|
-
* @param url {string} a Spaces Repository URL
|
|
403
|
-
* @returns {string | null}
|
|
404
|
-
*/
|
|
405
|
-
export function baseUrlOfSpacesUrl(url) {
|
|
406
|
-
const split = splitAtBase({
|
|
407
|
-
url,
|
|
408
|
-
boundary: segments => {
|
|
409
|
-
// Trailing empty segments are the collapsed run of slashes.
|
|
410
|
-
let last = segments.length - 1;
|
|
411
|
-
while (last >= 0 && segments[last] === '') {
|
|
412
|
-
last -= 1;
|
|
413
|
-
}
|
|
414
|
-
return segments[last] === 'spaces' ? last : -1;
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
return split?.baseUrl ?? null;
|
|
669
|
+
export function resourceChunkUrl({ spaceUrl, collectionId, resourceId, chunkIndex }) {
|
|
670
|
+
return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/chunks/${chunkIndex}`;
|
|
418
671
|
}
|
|
419
672
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* The path is joined onto `baseUrl`'s path rather than its origin, so a WAS
|
|
424
|
-
* deployment mounted under a sub-path (e.g. `https://host/was`) keeps that
|
|
425
|
-
* prefix. A bare-origin `baseUrl` behaves as before.
|
|
673
|
+
* `{spaceUrl}{collectionId}/{resourceId}/chunks/` -- the chunk container of a
|
|
674
|
+
* chunked Resource: its stored chunks, listed. Container form, trailing slash
|
|
675
|
+
* (the member form, {@link resourceChunkUrl}, has none).
|
|
426
676
|
*
|
|
427
677
|
* @param options {object}
|
|
428
|
-
* @param options.
|
|
429
|
-
* @param options.
|
|
678
|
+
* @param options.spaceUrl {string}
|
|
679
|
+
* @param options.collectionId {string}
|
|
680
|
+
* @param options.resourceId {string}
|
|
430
681
|
* @returns {string}
|
|
431
682
|
*/
|
|
432
|
-
export function
|
|
433
|
-
|
|
434
|
-
// any base-path prefix on `baseUrl`. Ensure the base ends in a slash and
|
|
435
|
-
// make the path relative so the prefix is preserved.
|
|
436
|
-
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
437
|
-
const relative = path.startsWith('/') ? path.slice(1) : path;
|
|
438
|
-
return new URL(relative, base).toString();
|
|
683
|
+
export function resourceChunksUrl({ spaceUrl, collectionId, resourceId }) {
|
|
684
|
+
return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/chunks/`;
|
|
439
685
|
}
|
|
440
686
|
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
687
|
+
* The canonical (trailing-slash) form of a container URL received from
|
|
688
|
+
* outside and requested as given: the Spaces Repository URL a service
|
|
689
|
+
* description advertises, a public Collection link. The bare form only
|
|
690
|
+
* 308-redirects, and a signed invocation does not survive the redirect, so
|
|
691
|
+
* the slash is added here before the URL is ever requested. This module owns
|
|
692
|
+
* the trailing-slash rules; callers normalize here rather than re-encoding
|
|
693
|
+
* the rule.
|
|
445
694
|
*
|
|
446
|
-
* @param
|
|
695
|
+
* @param url {string} an absolute container URL, in either form
|
|
447
696
|
* @returns {string}
|
|
448
697
|
*/
|
|
449
|
-
export function
|
|
450
|
-
|
|
698
|
+
export function canonicalContainerUrl(url) {
|
|
699
|
+
// The slash marks the end of the path, so it goes before any query or
|
|
700
|
+
// fragment; the rest of the URL is kept as given.
|
|
701
|
+
const pathEnd = url.search(/[?#]/);
|
|
702
|
+
const path = pathEnd === -1 ? url : url.slice(0, pathEnd);
|
|
703
|
+
if (path.endsWith('/')) {
|
|
704
|
+
return url;
|
|
705
|
+
}
|
|
706
|
+
return pathEnd === -1 ? `${url}/` : `${path}/${url.slice(pathEnd)}`;
|
|
451
707
|
}
|
|
452
708
|
/**
|
|
453
|
-
* Classifies an absolute `target` URL
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
709
|
+
* Classifies an absolute `target` URL by where it sits beneath `spaceUrl`:
|
|
710
|
+
* the Space itself, one of its Collections, a Resource, or a reserved
|
|
711
|
+
* sub-endpoint. The inverse of the builders above, kept next to them so the
|
|
712
|
+
* grammar is owned in one place: `WasClient.fromCapability` reads a
|
|
713
|
+
* capability's `invocationTarget` through it, and the builders stay in
|
|
714
|
+
* lockstep.
|
|
715
|
+
*
|
|
716
|
+
* Both URLs are compared in parsed form, so a target serialized by another
|
|
717
|
+
* party (an upper-case host, a default port) still matches. Segments are
|
|
718
|
+
* percent-decoded (the builders re-encode them). Empty segments are dropped,
|
|
719
|
+
* so a container addressed in either form classifies the same: the bare and
|
|
720
|
+
* the trailing-slash form of a Collection are both the Collection. That is
|
|
721
|
+
* deliberate: the builders emit only the canonical form, but the targets
|
|
722
|
+
* parsed here come from capabilities minted elsewhere, and a client that
|
|
723
|
+
* reads only one form would refuse valid ones.
|
|
457
724
|
*
|
|
458
|
-
* Returns `null` when `target` is not beneath `
|
|
459
|
-
* another
|
|
460
|
-
*
|
|
461
|
-
* an error.
|
|
725
|
+
* Returns `null` when `target` is not beneath `spaceUrl` (another origin, or
|
|
726
|
+
* another path), carries a query or fragment, or either URL is malformed.
|
|
727
|
+
* The caller chooses whether that is an error.
|
|
462
728
|
*
|
|
463
729
|
* @param options {object}
|
|
464
|
-
* @param options.
|
|
465
|
-
* @param options.target {string}
|
|
730
|
+
* @param options.spaceUrl {string} the Space URL
|
|
731
|
+
* @param options.target {string} an absolute URL expected beneath it
|
|
466
732
|
* @returns {ParsedSpacePath | null}
|
|
467
733
|
*/
|
|
468
|
-
export function parseSpaceTarget({
|
|
469
|
-
// Compare the parsed forms, not the strings: `baseUrl` is canonical (the
|
|
470
|
-
// constructor derives it through `URL`) while a target serialized by another
|
|
471
|
-
// party may write the host in upper case or carry a default port.
|
|
472
|
-
let url;
|
|
734
|
+
export function parseSpaceTarget({ spaceUrl, target }) {
|
|
473
735
|
let base;
|
|
474
736
|
try {
|
|
475
|
-
|
|
476
|
-
base = new URL(baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
737
|
+
base = new URL(canonicalSpaceUrl(spaceUrl));
|
|
477
738
|
}
|
|
478
739
|
catch {
|
|
479
740
|
return null;
|
|
480
741
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
url.origin !== base.origin ||
|
|
484
|
-
!url.pathname.startsWith(base.pathname)) {
|
|
742
|
+
const url = parseContainerUrl(target);
|
|
743
|
+
if (url === null || url.origin !== base.origin) {
|
|
485
744
|
return null;
|
|
486
745
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
* the base URL is read off the target itself, as everything before its first
|
|
492
|
-
* `space` path segment, so a WAS mounted under a sub-path
|
|
493
|
-
* (`https://host/was/space/...`) yields `https://host/was`. For a target on a
|
|
494
|
-
* known server, `parseSpaceTarget` is the stricter check (it refuses a target
|
|
495
|
-
* beneath any other base). This is the one for a capability received from
|
|
496
|
-
* another party, whose `invocationTarget` may name any host.
|
|
497
|
-
*
|
|
498
|
-
* Returns `null` when `target` is not an absolute `http(s)` URL, has no
|
|
499
|
-
* `space` segment, carries a query or fragment, or has an empty segment in
|
|
500
|
-
* the base before it (`https://host//was/space/x`); these are the rules
|
|
501
|
-
* `baseUrlOfSpacesUrl` applies too. Also `null` from the path grammar for a
|
|
502
|
-
* path it cannot classify. The caller chooses whether that is an error.
|
|
503
|
-
*
|
|
504
|
-
* @param target {string} an absolute WAS URL
|
|
505
|
-
* @returns {{ baseUrl: string; path: ParsedSpacePath } | null} the server
|
|
506
|
-
* base URL (no trailing slash) and the parsed target beneath it
|
|
507
|
-
*/
|
|
508
|
-
export function parseAbsoluteSpaceTarget(target) {
|
|
509
|
-
const split = splitAtBase({
|
|
510
|
-
url: target,
|
|
511
|
-
boundary: segments => segments.indexOf('space')
|
|
512
|
-
});
|
|
513
|
-
if (split === null) {
|
|
746
|
+
// The bare form of the Space itself (`.../space/s`, no slash) names the
|
|
747
|
+
// Space too, so the prefix check reads the path without its final slash.
|
|
748
|
+
const basePath = base.pathname.slice(0, -1);
|
|
749
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
|
|
514
750
|
return null;
|
|
515
751
|
}
|
|
516
|
-
const path = parseSpacePath(`/${split.rest.join('/')}`);
|
|
517
|
-
return path === null ? null : { baseUrl: split.baseUrl, path };
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Parses a server pathname back into the containment depth it addresses -- the
|
|
521
|
-
* inverse of the builders above, kept next to them so the grammar is owned in
|
|
522
|
-
* one place. Segments are percent-decoded (the builders re-encode them).
|
|
523
|
-
* Returns `null` for a pathname outside the `/space/...` tree; the caller
|
|
524
|
-
* chooses the error. A path that addresses a reserved sub-endpoint rather than
|
|
525
|
-
* a space/collection/resource -- e.g. `/space/s/policy`, `/space/s/c/backend`,
|
|
526
|
-
* or any 5-segment target like `/space/s/c/r/meta` -- is classified
|
|
527
|
-
* `sub-resource`, never silently truncated to the nearest handle.
|
|
528
|
-
*
|
|
529
|
-
* Empty segments are dropped, so a container addressed in either form
|
|
530
|
-
* classifies the same: `/space/s` and `/space/s/` are both the Space, and
|
|
531
|
-
* `/space/s/c` and `/space/s/c/` both the Collection. That is deliberate. The
|
|
532
|
-
* builders emit only the canonical trailing-slash form, but the targets parsed
|
|
533
|
-
* here come from capabilities minted elsewhere, and a client that reads only
|
|
534
|
-
* one spelling would refuse valid ones.
|
|
535
|
-
*
|
|
536
|
-
* @param pathname {string} a URL pathname (e.g. from `new URL(...).pathname`)
|
|
537
|
-
* @returns {ParsedSpacePath | null}
|
|
538
|
-
*/
|
|
539
|
-
export function parseSpacePath(pathname) {
|
|
540
752
|
let segments;
|
|
541
753
|
try {
|
|
542
|
-
segments = pathname
|
|
754
|
+
segments = url.pathname
|
|
755
|
+
.slice(basePath.length)
|
|
756
|
+
.split('/')
|
|
757
|
+
.filter(Boolean)
|
|
758
|
+
.map(decodeURIComponent);
|
|
543
759
|
}
|
|
544
760
|
catch {
|
|
545
761
|
// A malformed percent-escape (e.g. `%ff`) makes `decodeURIComponent` throw
|
|
546
|
-
// a `URIError`: the path is not a well-formed
|
|
762
|
+
// a `URIError`: the path is not a well-formed target, so classify it as
|
|
547
763
|
// unparseable (the caller converts `null` to its own typed error).
|
|
548
764
|
return null;
|
|
549
765
|
}
|
|
550
|
-
if (segments
|
|
551
|
-
return
|
|
552
|
-
}
|
|
553
|
-
const [, spaceId, ...rest] = segments;
|
|
554
|
-
if (rest.length === 0) {
|
|
555
|
-
return { kind: 'space', spaceId };
|
|
766
|
+
if (segments.length === 0) {
|
|
767
|
+
return { kind: 'space' };
|
|
556
768
|
}
|
|
557
769
|
// A reserved segment directly under the space (`policy`, `backends`,
|
|
558
770
|
// `export`, ...) addresses a space-level sub-endpoint, as does anything
|
|
559
|
-
// nested beneath one (
|
|
560
|
-
if (isReservedCollectionId(
|
|
561
|
-
|
|
771
|
+
// nested beneath one (`backends/{backendId}`).
|
|
772
|
+
if (isReservedCollectionId(segments[0], {
|
|
773
|
+
originRoot: base.pathname === '/'
|
|
774
|
+
})) {
|
|
775
|
+
return { kind: 'sub-resource', segments };
|
|
562
776
|
}
|
|
563
|
-
if (
|
|
564
|
-
return { kind: 'collection',
|
|
777
|
+
if (segments.length === 1) {
|
|
778
|
+
return { kind: 'collection', collectionId: segments[0] };
|
|
565
779
|
}
|
|
566
780
|
// A reserved segment under the collection (`policy`, `backend`, `quota`,
|
|
567
781
|
// ...) addresses a collection-level sub-endpoint.
|
|
568
|
-
if (isReservedResourceId(
|
|
569
|
-
return { kind: 'sub-resource',
|
|
782
|
+
if (isReservedResourceId(segments[1])) {
|
|
783
|
+
return { kind: 'sub-resource', segments };
|
|
570
784
|
}
|
|
571
|
-
if (
|
|
785
|
+
if (segments.length === 2) {
|
|
572
786
|
return {
|
|
573
787
|
kind: 'resource',
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
resourceId: rest[1]
|
|
788
|
+
collectionId: segments[0],
|
|
789
|
+
resourceId: segments[1]
|
|
577
790
|
};
|
|
578
791
|
}
|
|
579
|
-
// Anything deeper (
|
|
580
|
-
//
|
|
581
|
-
return { kind: 'sub-resource',
|
|
792
|
+
// Anything deeper (`{c}/{r}/meta`, `{c}/{r}/policy`, chunk sub-segments,
|
|
793
|
+
// ...) is a resource-level sub-endpoint.
|
|
794
|
+
return { kind: 'sub-resource', segments };
|
|
582
795
|
}
|
|
583
796
|
//# sourceMappingURL=paths.js.map
|