@interop/was-client 0.99.0 → 0.100.0

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.
Files changed (149) hide show
  1. package/README.md +94 -71
  2. package/dist/Collection.d.ts +20 -8
  3. package/dist/Collection.d.ts.map +1 -1
  4. package/dist/Collection.js +89 -69
  5. package/dist/Collection.js.map +1 -1
  6. package/dist/Resource.d.ts +14 -3
  7. package/dist/Resource.d.ts.map +1 -1
  8. package/dist/Resource.js +38 -27
  9. package/dist/Resource.js.map +1 -1
  10. package/dist/Space.d.ts +18 -4
  11. package/dist/Space.d.ts.map +1 -1
  12. package/dist/Space.js +64 -61
  13. package/dist/Space.js.map +1 -1
  14. package/dist/WasClient.d.ts +105 -58
  15. package/dist/WasClient.d.ts.map +1 -1
  16. package/dist/WasClient.js +216 -142
  17. package/dist/WasClient.js.map +1 -1
  18. package/dist/codec.d.ts +3 -2
  19. package/dist/codec.d.ts.map +1 -1
  20. package/dist/edv/EdvCodec.d.ts.map +1 -1
  21. package/dist/edv/EdvCodec.js +37 -26
  22. package/dist/edv/EdvCodec.js.map +1 -1
  23. package/dist/edv/WasTransport.d.ts +7 -4
  24. package/dist/edv/WasTransport.d.ts.map +1 -1
  25. package/dist/edv/WasTransport.js +39 -20
  26. package/dist/edv/WasTransport.js.map +1 -1
  27. package/dist/edv/acquire.d.ts +3 -3
  28. package/dist/edv/acquire.d.ts.map +1 -1
  29. package/dist/edv/acquire.js +3 -3
  30. package/dist/edv/acquire.js.map +1 -1
  31. package/dist/edv/cipher.d.ts +7 -4
  32. package/dist/edv/cipher.d.ts.map +1 -1
  33. package/dist/edv/cipher.js +7 -4
  34. package/dist/edv/cipher.js.map +1 -1
  35. package/dist/edv/core.d.ts +1 -1
  36. package/dist/edv/core.d.ts.map +1 -1
  37. package/dist/edv/core.js +1 -1
  38. package/dist/edv/core.js.map +1 -1
  39. package/dist/edv/docCipher.d.ts +10 -7
  40. package/dist/edv/docCipher.d.ts.map +1 -1
  41. package/dist/edv/docCipher.js +12 -8
  42. package/dist/edv/docCipher.js.map +1 -1
  43. package/dist/edv/encryption.d.ts +2 -2
  44. package/dist/edv/encryption.d.ts.map +1 -1
  45. package/dist/edv/encryption.js +8 -6
  46. package/dist/edv/encryption.js.map +1 -1
  47. package/dist/edv/epochCrypto.d.ts +12 -4
  48. package/dist/edv/epochCrypto.d.ts.map +1 -1
  49. package/dist/edv/epochCrypto.js +37 -10
  50. package/dist/edv/epochCrypto.js.map +1 -1
  51. package/dist/edv/epochKeys.d.ts +6 -2
  52. package/dist/edv/epochKeys.d.ts.map +1 -1
  53. package/dist/edv/epochKeys.js +25 -14
  54. package/dist/edv/epochKeys.js.map +1 -1
  55. package/dist/edv/hmacKey.d.ts +7 -1
  56. package/dist/edv/hmacKey.d.ts.map +1 -1
  57. package/dist/edv/hmacKey.js +9 -3
  58. package/dist/edv/hmacKey.js.map +1 -1
  59. package/dist/edv/index.d.ts +1 -1
  60. package/dist/edv/index.d.ts.map +1 -1
  61. package/dist/edv/index.js +1 -1
  62. package/dist/edv/index.js.map +1 -1
  63. package/dist/edv/recipients.d.ts.map +1 -1
  64. package/dist/edv/recipients.js +2 -0
  65. package/dist/edv/recipients.js.map +1 -1
  66. package/dist/edv/refreshingDocCipher.d.ts +3 -3
  67. package/dist/edv/refreshingDocCipher.d.ts.map +1 -1
  68. package/dist/edv/refreshingDocCipher.js +3 -3
  69. package/dist/edv/refreshingDocCipher.js.map +1 -1
  70. package/dist/edv/transportFactory.d.ts +3 -3
  71. package/dist/edv/transportFactory.d.ts.map +1 -1
  72. package/dist/edv/transportFactory.js +3 -3
  73. package/dist/edv/transportFactory.js.map +1 -1
  74. package/dist/errors.d.ts +6 -4
  75. package/dist/errors.d.ts.map +1 -1
  76. package/dist/errors.js +6 -4
  77. package/dist/errors.js.map +1 -1
  78. package/dist/index.d.ts +2 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/internal/codec.d.ts +7 -7
  83. package/dist/internal/codec.d.ts.map +1 -1
  84. package/dist/internal/codec.js +19 -18
  85. package/dist/internal/codec.js.map +1 -1
  86. package/dist/internal/conditional.d.ts +3 -3
  87. package/dist/internal/conditional.d.ts.map +1 -1
  88. package/dist/internal/conditional.js +5 -5
  89. package/dist/internal/conditional.js.map +1 -1
  90. package/dist/internal/grant.d.ts +15 -26
  91. package/dist/internal/grant.d.ts.map +1 -1
  92. package/dist/internal/grant.js +95 -29
  93. package/dist/internal/grant.js.map +1 -1
  94. package/dist/internal/memo.d.ts +13 -4
  95. package/dist/internal/memo.d.ts.map +1 -1
  96. package/dist/internal/memo.js +18 -7
  97. package/dist/internal/memo.js.map +1 -1
  98. package/dist/internal/meta.d.ts +8 -8
  99. package/dist/internal/meta.d.ts.map +1 -1
  100. package/dist/internal/meta.js +13 -10
  101. package/dist/internal/meta.js.map +1 -1
  102. package/dist/internal/pagination.d.ts +30 -15
  103. package/dist/internal/pagination.d.ts.map +1 -1
  104. package/dist/internal/pagination.js +31 -17
  105. package/dist/internal/pagination.js.map +1 -1
  106. package/dist/internal/paths.d.ts +332 -195
  107. package/dist/internal/paths.d.ts.map +1 -1
  108. package/dist/internal/paths.js +400 -367
  109. package/dist/internal/paths.js.map +1 -1
  110. package/dist/internal/policy.d.ts +23 -23
  111. package/dist/internal/policy.d.ts.map +1 -1
  112. package/dist/internal/policy.js +20 -20
  113. package/dist/internal/policy.js.map +1 -1
  114. package/dist/internal/request.d.ts +17 -21
  115. package/dist/internal/request.d.ts.map +1 -1
  116. package/dist/internal/request.js +10 -21
  117. package/dist/internal/request.js.map +1 -1
  118. package/dist/internal/revoke.d.ts +34 -13
  119. package/dist/internal/revoke.d.ts.map +1 -1
  120. package/dist/internal/revoke.js +97 -28
  121. package/dist/internal/revoke.js.map +1 -1
  122. package/dist/internal/service.d.ts +25 -7
  123. package/dist/internal/service.d.ts.map +1 -1
  124. package/dist/internal/service.js +49 -45
  125. package/dist/internal/service.js.map +1 -1
  126. package/dist/internal/write.d.ts +18 -18
  127. package/dist/internal/write.d.ts.map +1 -1
  128. package/dist/internal/write.js +27 -27
  129. package/dist/internal/write.js.map +1 -1
  130. package/dist/paths.d.ts +32 -25
  131. package/dist/paths.d.ts.map +1 -1
  132. package/dist/paths.js +32 -25
  133. package/dist/paths.js.map +1 -1
  134. package/dist/sync/index.d.ts +4 -2
  135. package/dist/sync/index.d.ts.map +1 -1
  136. package/dist/sync/index.js +4 -2
  137. package/dist/sync/index.js.map +1 -1
  138. package/dist/sync/port.d.ts +3 -3
  139. package/dist/sync/port.d.ts.map +1 -1
  140. package/dist/sync/port.js +12 -12
  141. package/dist/sync/port.js.map +1 -1
  142. package/dist/sync/provisioning.d.ts +10 -9
  143. package/dist/sync/provisioning.d.ts.map +1 -1
  144. package/dist/sync/provisioning.js +19 -18
  145. package/dist/sync/provisioning.js.map +1 -1
  146. package/dist/sync/types.d.ts +2 -2
  147. package/dist/types.d.ts +13 -5
  148. package/dist/types.d.ts.map +1 -1
  149. package/package.json +4 -4
@@ -2,36 +2,37 @@
2
2
  * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
3
  */
4
4
  /**
5
- * URL path builders for the WAS server, encoding the trailing-slash
6
- * canonicalization once. The convention: a trailing slash marks a container in
7
- * canonical form (`/space/{s}/`, `/space/{s}/{c}/`), everything else has
8
- * none, and no two paths differ only by a trailing slash. Path segments are
9
- * percent-encoded so ids never break out of their slot. The Spaces Repository
10
- * has no builder: its URL is the `spaces` member of the service description.
5
+ * URL builders for the hierarchy beneath a Space, encoding the trailing-slash
6
+ * canonicalization once. Every URL this client requests is derived from a
7
+ * Space URL the server handed out (the `Location` of the create response, a
8
+ * listing item, a linkset, or the caller's stored pointer) and from nothing
9
+ * above it: the spec places no Space beside the Spaces Repository, so no
10
+ * builder here forms a Space URL, and none takes a server root. The Spaces
11
+ * Repository URL is the `spaces` member of the service description.
12
+ *
13
+ * The convention: a trailing slash marks a container in canonical form (the
14
+ * Space URL itself, and `{spaceUrl}{collectionId}/`), everything else has
15
+ * none, and no two URLs differ only by a trailing slash. Path segments are
16
+ * percent-encoded so ids never break out of their slot.
11
17
  *
12
18
  * A container's own description is not at its URL: it lives at the reserved
13
- * `meta` segment (`spaceMeta`, `collectionMeta`), beside the Resource-level
14
- * `resourceMeta`. The bare (slash-less) form of a container URL only
15
- * 308-redirects, and a signed invocation covers the target it was signed for,
16
- * so no builder here emits the bare form -- a redirect would have to be signed
17
- * again.
18
- *
19
- * The parser is deliberately laxer than the builders. `parseSpacePath` accepts
20
- * the bare form too, because the targets it classifies are not this client's
21
- * own URLs: they are the `invocationTarget` strings of delegated capabilities
22
- * a third party minted, and those name containers in either form. Reading one
23
- * as the Space or Collection it addresses is what `WasClient.fromCapability`
24
- * and the revocation route's `spaceIdOf` do; refusing the bare form there would
25
- * reject valid capabilities over a spelling this client does not control.
19
+ * `meta` segment (`spaceMetaUrl`, `collectionMetaUrl`), beside the
20
+ * Resource-level `resourceMetaUrl`. The bare (slash-less) form of a container
21
+ * URL only 308-redirects, and a signed invocation covers the target it was
22
+ * signed for, so no builder here emits the bare form -- a redirect would have
23
+ * to be signed again.
24
+ *
25
+ * The parser is deliberately laxer than the builders. `parseSpaceTarget`
26
+ * accepts the bare container form too, because the targets it classifies are
27
+ * not this client's own URLs: they are the `invocationTarget` strings of
28
+ * delegated capabilities a third party minted, and those name containers in
29
+ * either form. Reading one as the Collection or Resource it addresses is what
30
+ * `WasClient.fromCapability` does; refusing the bare form there would reject
31
+ * valid capabilities over a form this client does not control.
26
32
  *
27
33
  * The zcap `invocationTarget` is derived from the request URL, so these
28
34
  * trailing-slash rules must match the server's per-operation `allowedTarget`
29
35
  * 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
36
  */
36
37
  import { ValidationError } from '../errors.js';
37
38
  import { assertNotReserved, isReservedCollectionId, isReservedResourceId } from './reserved.js';
@@ -58,10 +59,10 @@ function encode(segment) {
58
59
  }
59
60
  /**
60
61
  * Encodes a collection-id slot, additionally rejecting an id from the Reserved
61
- * Path Segment Registry: `collectionPath(s, 'policy')` is byte-identical to
62
- * the space policy path, so a reserved id would silently mis-target a
63
- * space-level endpoint. Guarding in the builders covers every URL-forming
64
- * entry point (handle constructors guard too, for the earlier failure).
62
+ * Path Segment Registry: `collectionUrl` of `policy` is byte-identical to the
63
+ * space policy URL, so a reserved id would silently mis-target a space-level
64
+ * endpoint. Guarding in the builders covers every URL-forming entry point
65
+ * (handle constructors guard too, for the earlier failure).
65
66
  *
66
67
  * @param collectionId {string}
67
68
  * @returns {string}
@@ -72,8 +73,8 @@ function encodeCollectionId(collectionId) {
72
73
  }
73
74
  /**
74
75
  * Encodes a resource-id slot, additionally rejecting an id from the Reserved
75
- * Path Segment Registry: `resourcePath(s, c, 'policy')` is byte-identical to
76
- * the collection policy path, so a reserved id would silently mis-target a
76
+ * Path Segment Registry: `resourceUrl` of `policy` is byte-identical to the
77
+ * collection policy URL, so a reserved id would silently mis-target a
77
78
  * collection-level endpoint.
78
79
  *
79
80
  * @param resourceId {string}
@@ -84,500 +85,532 @@ function encodeResourceId(resourceId) {
84
85
  return encode(resourceId);
85
86
  }
86
87
  /**
87
- * `/space/:spaceId` -- the slash-less prefix the space-level sub-endpoints hang
88
- * off. Module-private: the bare form is not a path this client ever requests
89
- * (the server only 308-redirects it), so it is a prefix here and never a
90
- * builder's return value. It is still a form the parser below reads, since a
91
- * third party's capability target may be written that way.
88
+ * Parses an absolute `http(s)` URL, or returns `null`. The one rule for which
89
+ * URLs may name a server, a Spaces Repository, or a container here; the
90
+ * callers that need more (no query, a path) narrow it.
92
91
  *
93
- * @param spaceId {string}
92
+ * @param url {string}
93
+ * @returns {URL | null}
94
+ */
95
+ export function parseAbsoluteHttpUrl(url) {
96
+ let parsed;
97
+ try {
98
+ parsed = new URL(url);
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
104
+ return null;
105
+ }
106
+ return parsed;
107
+ }
108
+ /**
109
+ * {@link parseAbsoluteHttpUrl} narrowed to a URL with no query or fragment:
110
+ * the rule for which URLs may name a container.
111
+ *
112
+ * @param url {string}
113
+ * @returns {URL | null}
114
+ */
115
+ function parseContainerUrl(url) {
116
+ const parsed = parseAbsoluteHttpUrl(url);
117
+ if (parsed === null || parsed.search !== '' || parsed.hash !== '') {
118
+ return null;
119
+ }
120
+ return parsed;
121
+ }
122
+ /**
123
+ * The Space URL in canonical form: an absolute `http(s)` URL, serialized by
124
+ * `URL` (lower-case host, default port dropped), ending in exactly one
125
+ * slash. A server names a Space in this form (the `Location` of the create
126
+ * response, a listing item's `url`), and every URL beneath the Space, and
127
+ * every zcap `invocationTarget`, is derived from it, so a caller's own
128
+ * serialization of the same Space must reduce to the same string. A missing
129
+ * trailing slash is added, since the bare form names the same container; a
130
+ * run of trailing slashes is collapsed to one.
131
+ *
132
+ * @param spaceUrl {string} a Space URL, as the server named it or as the
133
+ * caller stored it
134
+ * @returns {string}
135
+ * @throws {ValidationError} when `spaceUrl` is not an absolute `http(s)`
136
+ * URL, carries a query or fragment, or has no path segment (an origin is
137
+ * not a Space)
138
+ */
139
+ export function canonicalSpaceUrl(spaceUrl) {
140
+ const parsed = parseContainerUrl(spaceUrl);
141
+ if (parsed === null) {
142
+ throw new ValidationError(`"${spaceUrl}" is not a Space URL: an absolute http(s) URL with no ` +
143
+ 'query or fragment is required.');
144
+ }
145
+ const pathname = parsed.pathname.replace(/\/+$/, '');
146
+ if (pathname === '' || pathname.split('/').slice(1).includes('')) {
147
+ throw new ValidationError(`"${spaceUrl}" is not a Space URL: a Space has at least one path ` +
148
+ 'segment beneath its origin, with no empty segment.');
149
+ }
150
+ return `${parsed.origin}${pathname}/`;
151
+ }
152
+ /**
153
+ * The Space's id as its URL names it: the last path segment of the Space
154
+ * URL, percent-decoded. The spec's hierarchy and its did:webvh profile both
155
+ * place the id there (`.../space/{space_id}/`), and it is the value the Space
156
+ * Metadata object's `id` carries.
157
+ *
158
+ * @param spaceUrl {string} a Space URL
94
159
  * @returns {string}
160
+ * @throws {ValidationError} when `spaceUrl` is not a Space URL (see
161
+ * {@link canonicalSpaceUrl}) or its last segment is a malformed percent
162
+ * escape
95
163
  */
96
- function spacePrefix(spaceId) {
97
- return `/space/${encode(spaceId)}`;
164
+ export function spaceIdOfUrl(spaceUrl) {
165
+ return spaceIdOfCanonicalUrl(canonicalSpaceUrl(spaceUrl));
98
166
  }
99
167
  /**
100
- * `/space/:spaceId/` -- the Space container in canonical form: the target a
101
- * Space root capability names, the URL whose `GET` lists the Space's
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.
168
+ * {@link spaceIdOfUrl} for a Space URL already in canonical form (a handle's
169
+ * `url`, the result of {@link canonicalSpaceUrl}), sparing the second parse.
105
170
  *
106
- * @param spaceId {string}
171
+ * @param canonical {string} a canonical Space URL
107
172
  * @returns {string}
173
+ * @throws {ValidationError} when the last segment is a malformed percent
174
+ * escape
108
175
  */
109
- export function spacePath(spaceId) {
110
- return `${spacePrefix(spaceId)}/`;
176
+ export function spaceIdOfCanonicalUrl(canonical) {
177
+ const segment = canonical.split('/').filter(Boolean).pop();
178
+ try {
179
+ return decodeURIComponent(segment);
180
+ }
181
+ catch (err) {
182
+ throw new ValidationError(`"${canonical}" is not a Space URL: its last segment is not a ` +
183
+ 'well-formed percent-encoded id.', { cause: err });
184
+ }
111
185
  }
112
186
  /**
113
- * `/space/:spaceId/meta` -- the Space Metadata object: the Space's description
187
+ * `{spaceUrl}meta` -- the Space Metadata object: the Space's description
114
188
  * (`name`, `controller`, `type`), read with `GET` and replaced with `PUT`.
189
+ * The Space container itself (the canonical Space URL) is the target a Space
190
+ * root capability names, the URL whose `GET` lists the Space's Collections,
191
+ * whose `POST` creates one, and whose `DELETE` removes the Space; it has no
192
+ * builder, since the caller holds it.
115
193
  *
116
- * @param spaceId {string}
194
+ * @param options {object}
195
+ * @param options.spaceUrl {string}
117
196
  * @returns {string}
118
197
  */
119
- export function spaceMeta(spaceId) {
120
- return `${spacePrefix(spaceId)}/meta`;
198
+ export function spaceMetaUrl({ spaceUrl }) {
199
+ return `${canonicalSpaceUrl(spaceUrl)}meta`;
121
200
  }
122
201
  /**
123
- * `/space/:spaceId/export` -- export a space as a tar archive.
202
+ * `{spaceUrl}export` -- export a space as a tar archive.
203
+ *
204
+ * @param options {object}
205
+ * @param options.spaceUrl {string}
206
+ * @returns {string}
124
207
  */
125
- export function spaceExport(spaceId) {
126
- return `${spacePrefix(spaceId)}/export`;
208
+ export function spaceExportUrl({ spaceUrl }) {
209
+ return `${canonicalSpaceUrl(spaceUrl)}export`;
127
210
  }
128
211
  /**
129
- * `/space/:spaceId/import` -- import a tar archive into a space.
212
+ * `{spaceUrl}import` -- import a tar archive into a space.
213
+ *
214
+ * @param options {object}
215
+ * @param options.spaceUrl {string}
216
+ * @returns {string}
130
217
  */
131
- export function spaceImport(spaceId) {
132
- return `${spacePrefix(spaceId)}/import`;
218
+ export function spaceImportUrl({ spaceUrl }) {
219
+ return `${canonicalSpaceUrl(spaceUrl)}import`;
133
220
  }
134
221
  /**
135
- * `/space/:spaceId/backends` -- the backends available within a space (list, and
222
+ * `{spaceUrl}backends` -- the backends available within a space (list, and
136
223
  * register a new `external` backend).
224
+ *
225
+ * @param options {object}
226
+ * @param options.spaceUrl {string}
227
+ * @returns {string}
137
228
  */
138
- export function spaceBackends(spaceId) {
139
- return `${spacePrefix(spaceId)}/backends`;
229
+ export function spaceBackendsUrl({ spaceUrl }) {
230
+ return `${canonicalSpaceUrl(spaceUrl)}backends`;
140
231
  }
141
232
  /**
142
- * `/space/:spaceId/backends/:backendId` -- a single registered `external`
143
- * backend (replace / deregister by id).
233
+ * `{spaceUrl}backends/{backendId}` -- a single registered `external` backend
234
+ * (replace / deregister by id).
235
+ *
236
+ * @param options {object}
237
+ * @param options.spaceUrl {string}
238
+ * @param options.backendId {string}
239
+ * @returns {string}
144
240
  */
145
- export function registeredBackend(spaceId, backendId) {
146
- return `${spaceBackends(spaceId)}/${encode(backendId)}`;
241
+ export function registeredBackendUrl({ spaceUrl, backendId }) {
242
+ return `${spaceBackendsUrl({ spaceUrl })}/${encode(backendId)}`;
147
243
  }
148
244
  /**
149
- * `/space/:spaceId/quotas` -- the space-level storage quota report.
245
+ * `{spaceUrl}quotas` -- the space-level storage quota report.
246
+ *
247
+ * @param options {object}
248
+ * @param options.spaceUrl {string}
249
+ * @returns {string}
150
250
  */
151
- export function spaceQuotas(spaceId) {
152
- return `${spacePrefix(spaceId)}/quotas`;
251
+ export function spaceQuotasUrl({ spaceUrl }) {
252
+ return `${canonicalSpaceUrl(spaceUrl)}quotas`;
153
253
  }
154
254
  /**
155
- * `/space/:spaceId/policy` -- the space-level access-control policy resource.
255
+ * `{spaceUrl}policy` -- the space-level access-control policy resource.
256
+ *
257
+ * @param options {object}
258
+ * @param options.spaceUrl {string}
259
+ * @returns {string}
156
260
  */
157
- export function spacePolicy(spaceId) {
158
- return `${spacePrefix(spaceId)}/policy`;
261
+ export function spacePolicyUrl({ spaceUrl }) {
262
+ return `${canonicalSpaceUrl(spaceUrl)}policy`;
159
263
  }
160
264
  /**
161
- * `/space/:spaceId/linkset` -- the space-level linkset (policy discovery).
265
+ * `{spaceUrl}linkset` -- the space-level linkset (policy discovery).
266
+ *
267
+ * @param options {object}
268
+ * @param options.spaceUrl {string}
269
+ * @returns {string}
162
270
  */
163
- export function spaceLinkset(spaceId) {
164
- return `${spacePrefix(spaceId)}/linkset`;
271
+ export function spaceLinksetUrl({ spaceUrl }) {
272
+ return `${canonicalSpaceUrl(spaceUrl)}linkset`;
165
273
  }
166
274
  /**
167
- * `/space/:spaceId/replicas` -- the Space's replica registrations: `GET` lists
168
- * them and `POST` registers one. Controller-only, both forms.
275
+ * `{spaceUrl}replicas` -- the Space's replica registrations: `GET` lists them
276
+ * and `POST` registers one. Controller-only, both forms.
277
+ *
278
+ * @param options {object}
279
+ * @param options.spaceUrl {string}
280
+ * @returns {string}
169
281
  */
170
- export function spaceReplicas(spaceId) {
171
- return `${spacePrefix(spaceId)}/replicas`;
282
+ export function spaceReplicasUrl({ spaceUrl }) {
283
+ return `${canonicalSpaceUrl(spaceUrl)}replicas`;
172
284
  }
173
285
  /**
174
- * `/space/:spaceId/replicas/:replicaId` -- one replica registration (read and
286
+ * `{spaceUrl}replicas/{replicaId}` -- one replica registration (read and
175
287
  * delete by id).
288
+ *
289
+ * @param options {object}
290
+ * @param options.spaceUrl {string}
291
+ * @param options.replicaId {string}
292
+ * @returns {string}
176
293
  */
177
- export function replicaRegistration(spaceId, replicaId) {
178
- return `${spaceReplicas(spaceId)}/${encode(replicaId)}`;
294
+ export function replicaRegistrationUrl({ spaceUrl, replicaId }) {
295
+ return `${spaceReplicasUrl({ spaceUrl })}/${encode(replicaId)}`;
179
296
  }
180
297
  /**
181
- * `/space/:spaceId/replicas/:replicaId/status` -- the pull loop's runtime
182
- * state for one registration.
298
+ * `{spaceUrl}replicas/{replicaId}/status` -- the pull loop's runtime state
299
+ * for one registration.
300
+ *
301
+ * @param options {object}
302
+ * @param options.spaceUrl {string}
303
+ * @param options.replicaId {string}
304
+ * @returns {string}
183
305
  */
184
- export function replicaStatus(spaceId, replicaId) {
185
- return `${replicaRegistration(spaceId, replicaId)}/status`;
306
+ export function replicaStatusUrl({ spaceUrl, replicaId }) {
307
+ return `${replicaRegistrationUrl({ spaceUrl, replicaId })}/status`;
186
308
  }
187
309
  /**
188
- * `/space/:spaceId/zcaps/revocations/:capabilityId` -- submit a revocation of a
310
+ * `{spaceUrl}zcaps/revocations/{capabilityId}` -- submit a revocation of a
189
311
  * Space-rooted capability. The capability's `id` (typically a `urn:uuid:`) is
190
312
  * percent-encoded into the single final segment. `zcaps` is in the Reserved
191
313
  * Path Segment Registry, so no Collection can take that slot.
314
+ *
315
+ * @param options {object}
316
+ * @param options.spaceUrl {string}
317
+ * @param options.capabilityId {string}
318
+ * @returns {string}
192
319
  */
193
- export function spaceRevocation(spaceId, capabilityId) {
194
- return `${spacePrefix(spaceId)}/zcaps/revocations/${encode(capabilityId)}`;
320
+ export function spaceRevocationUrl({ spaceUrl, capabilityId }) {
321
+ return `${canonicalSpaceUrl(spaceUrl)}zcaps/revocations/${encode(capabilityId)}`;
195
322
  }
196
323
  /**
197
- * `/space/:spaceId/:collectionId` -- the slash-less prefix the collection-level
198
- * sub-endpoints and the Resource ids hang off. Module-private, like
199
- * {@link spacePrefix}.
324
+ * `{spaceUrl}{collectionId}` -- the slash-less prefix the collection-level
325
+ * sub-endpoints and the Resource ids hang off. Module-private: the bare form
326
+ * is not a URL this client ever requests (the server only 308-redirects it),
327
+ * so it is a prefix here and never a builder's return value.
200
328
  *
201
- * @param spaceId {string}
202
- * @param collectionId {string}
329
+ * @param options {object}
330
+ * @param options.spaceUrl {string}
331
+ * @param options.collectionId {string}
203
332
  * @returns {string}
204
333
  */
205
- function collectionPrefix(spaceId, collectionId) {
206
- return `${spacePrefix(spaceId)}/${encodeCollectionId(collectionId)}`;
334
+ function collectionPrefix({ spaceUrl, collectionId }) {
335
+ return `${canonicalSpaceUrl(spaceUrl)}${encodeCollectionId(collectionId)}`;
207
336
  }
208
337
  /**
209
- * `/space/:spaceId/:collectionId/` -- the Collection container in canonical
210
- * form: the target a "share this collection" capability names, the URL whose
211
- * `GET` lists its Resources, whose `POST` adds one, and whose `DELETE` removes
212
- * the Collection.
338
+ * `{spaceUrl}{collectionId}/` -- the Collection container in canonical form:
339
+ * the target a "share this collection" capability names, the URL whose `GET`
340
+ * lists its Resources, whose `POST` adds one, and whose `DELETE` removes the
341
+ * Collection.
213
342
  *
214
- * @param spaceId {string}
215
- * @param collectionId {string}
343
+ * @param options {object}
344
+ * @param options.spaceUrl {string}
345
+ * @param options.collectionId {string}
216
346
  * @returns {string}
217
347
  */
218
- export function collectionPath(spaceId, collectionId) {
219
- return `${collectionPrefix(spaceId, collectionId)}/`;
348
+ export function collectionUrl({ spaceUrl, collectionId }) {
349
+ return `${collectionPrefix({ spaceUrl, collectionId })}/`;
220
350
  }
221
351
  /**
222
- * `/space/:spaceId/:collectionId/policy` -- the collection-level access-control
352
+ * `{spaceUrl}{collectionId}/policy` -- the collection-level access-control
223
353
  * policy resource.
354
+ *
355
+ * @param options {object}
356
+ * @param options.spaceUrl {string}
357
+ * @param options.collectionId {string}
358
+ * @returns {string}
224
359
  */
225
- export function collectionPolicy(spaceId, collectionId) {
226
- return `${collectionPrefix(spaceId, collectionId)}/policy`;
360
+ export function collectionPolicyUrl({ spaceUrl, collectionId }) {
361
+ return `${collectionPrefix({ spaceUrl, collectionId })}/policy`;
227
362
  }
228
363
  /**
229
- * `/space/:spaceId/:collectionId/linkset` -- the collection-level linkset
230
- * (policy discovery).
364
+ * `{spaceUrl}{collectionId}/linkset` -- the collection-level linkset (policy
365
+ * discovery).
366
+ *
367
+ * @param options {object}
368
+ * @param options.spaceUrl {string}
369
+ * @param options.collectionId {string}
370
+ * @returns {string}
231
371
  */
232
- export function collectionLinkset(spaceId, collectionId) {
233
- return `${collectionPrefix(spaceId, collectionId)}/linkset`;
372
+ export function collectionLinksetUrl({ spaceUrl, collectionId }) {
373
+ return `${collectionPrefix({ spaceUrl, collectionId })}/linkset`;
234
374
  }
235
375
  /**
236
- * `/space/:spaceId/:collectionId/backend` -- the "Collection Backend Selected"
376
+ * `{spaceUrl}{collectionId}/backend` -- the "Collection Backend Selected"
237
377
  * descriptor (the backend this collection is stored on).
378
+ *
379
+ * @param options {object}
380
+ * @param options.spaceUrl {string}
381
+ * @param options.collectionId {string}
382
+ * @returns {string}
238
383
  */
239
- export function collectionBackend(spaceId, collectionId) {
240
- return `${collectionPrefix(spaceId, collectionId)}/backend`;
384
+ export function collectionBackendUrl({ spaceUrl, collectionId }) {
385
+ return `${collectionPrefix({ spaceUrl, collectionId })}/backend`;
241
386
  }
242
387
  /**
243
- * `/space/:spaceId/:collectionId/quota` -- the per-collection storage quota
244
- * report (spec "Quotas").
388
+ * `{spaceUrl}{collectionId}/quota` -- the per-collection storage quota report
389
+ * (spec "Quotas").
390
+ *
391
+ * @param options {object}
392
+ * @param options.spaceUrl {string}
393
+ * @param options.collectionId {string}
394
+ * @returns {string}
245
395
  */
246
- export function collectionQuota(spaceId, collectionId) {
247
- return `${collectionPrefix(spaceId, collectionId)}/quota`;
396
+ export function collectionQuotaUrl({ spaceUrl, collectionId }) {
397
+ return `${collectionPrefix({ spaceUrl, collectionId })}/quota`;
248
398
  }
249
399
  /**
250
- * `/space/:spaceId/:collectionId/meta` -- the Collection Metadata object: the
400
+ * `{spaceUrl}{collectionId}/meta` -- the Collection Metadata object: the
251
401
  * Collection's configuration (`name`, `backend`, `encryption`, `generator`)
252
402
  * together with the server-managed timestamps / `createdBy` and the
253
403
  * user-writable `custom` and `epoch`, as one object under one validator.
404
+ *
405
+ * @param options {object}
406
+ * @param options.spaceUrl {string}
407
+ * @param options.collectionId {string}
408
+ * @returns {string}
254
409
  */
255
- export function collectionMeta(spaceId, collectionId) {
256
- return `${collectionPrefix(spaceId, collectionId)}/meta`;
410
+ export function collectionMetaUrl({ spaceUrl, collectionId }) {
411
+ return `${collectionPrefix({ spaceUrl, collectionId })}/meta`;
257
412
  }
258
413
  /**
259
- * `/space/:spaceId/:collectionId/meta/log` -- the Collection's governing
260
- * history log sub-resource (JSON Lines). Not a Resource of the Collection:
261
- * absent from listings and the `changes` feed, and versioned by its own ETag.
414
+ * `{spaceUrl}{collectionId}/meta/log` -- the Collection's governing history
415
+ * log sub-resource (JSON Lines). Not a Resource of the Collection: absent from
416
+ * listings and the `changes` feed, and versioned by its own ETag.
417
+ *
418
+ * @param options {object}
419
+ * @param options.spaceUrl {string}
420
+ * @param options.collectionId {string}
421
+ * @returns {string}
262
422
  */
263
- export function collectionLog(spaceId, collectionId) {
264
- return `${collectionMeta(spaceId, collectionId)}/log`;
423
+ export function collectionLogUrl({ spaceUrl, collectionId }) {
424
+ return `${collectionMetaUrl({ spaceUrl, collectionId })}/log`;
265
425
  }
266
426
  /**
267
- * `/space/:spaceId/:collectionId/query` -- the collection-level query endpoint,
427
+ * `{spaceUrl}{collectionId}/query` -- the collection-level query endpoint,
268
428
  * whose body's `profile` selects the query (e.g. `changes`, `blinded-index`).
429
+ *
430
+ * @param options {object}
431
+ * @param options.spaceUrl {string}
432
+ * @param options.collectionId {string}
433
+ * @returns {string}
269
434
  */
270
- export function collectionQuery(spaceId, collectionId) {
271
- return `${collectionPrefix(spaceId, collectionId)}/query`;
435
+ export function collectionQueryUrl({ spaceUrl, collectionId }) {
436
+ return `${collectionPrefix({ spaceUrl, collectionId })}/query`;
272
437
  }
273
438
  /**
274
- * `/space/:spaceId/:collectionId/:resourceId` -- get / put / delete a resource
439
+ * `{spaceUrl}{collectionId}/{resourceId}` -- get / put / delete a resource
275
440
  * (no trailing slash).
441
+ *
442
+ * @param options {object}
443
+ * @param options.spaceUrl {string}
444
+ * @param options.collectionId {string}
445
+ * @param options.resourceId {string}
446
+ * @returns {string}
276
447
  */
277
- export function resourcePath(spaceId, collectionId, resourceId) {
278
- return `${collectionPrefix(spaceId, collectionId)}/${encodeResourceId(resourceId)}`;
448
+ export function resourceUrl({ spaceUrl, collectionId, resourceId }) {
449
+ return `${collectionPrefix({ spaceUrl, collectionId })}/${encodeResourceId(resourceId)}`;
279
450
  }
280
451
  /**
281
- * `/space/:spaceId/:collectionId/:resourceId/meta` -- the resource metadata
452
+ * `{spaceUrl}{collectionId}/{resourceId}/meta` -- the resource metadata
282
453
  * 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
454
  *
301
- * @param spaceId {string}
302
- * @param collectionId {string}
303
- * @param resourceId {string}
304
- * @param chunkIndex {number}
455
+ * @param options {object}
456
+ * @param options.spaceUrl {string}
457
+ * @param options.collectionId {string}
458
+ * @param options.resourceId {string}
305
459
  * @returns {string}
306
460
  */
307
- export function resourceChunkPath(spaceId, collectionId, resourceId, chunkIndex) {
308
- return `${resourcePath(spaceId, collectionId, resourceId)}/chunks/${chunkIndex}`;
461
+ export function resourceMetaUrl({ spaceUrl, collectionId, resourceId }) {
462
+ return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/meta`;
309
463
  }
310
464
  /**
311
- * `/space/:spaceId/:collectionId/:resourceId/chunks/` -- the chunk container
312
- * of a chunked Resource: its stored chunks, listed. Container form, trailing
313
- * slash (the member form, {@link resourceChunkPath}, has none).
465
+ * `{spaceUrl}{collectionId}/{resourceId}/policy` -- the resource-level
466
+ * access-control policy resource.
314
467
  *
315
- * @param spaceId {string}
316
- * @param collectionId {string}
317
- * @param resourceId {string}
468
+ * @param options {object}
469
+ * @param options.spaceUrl {string}
470
+ * @param options.collectionId {string}
471
+ * @param options.resourceId {string}
318
472
  * @returns {string}
319
473
  */
320
- export function resourceChunksPath(spaceId, collectionId, resourceId) {
321
- return `${resourcePath(spaceId, collectionId, resourceId)}/chunks/`;
474
+ export function resourcePolicyUrl({ spaceUrl, collectionId, resourceId }) {
475
+ return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/policy`;
322
476
  }
323
477
  /**
324
- * The Spaces Repository URL beneath a server base URL: the reserved `spaces`
325
- * segment at the server root, in canonical trailing-slash form. This is the
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.
478
+ * `{spaceUrl}{collectionId}/{resourceId}/chunks/{chunkIndex}` -- a single
479
+ * stored chunk of a chunked Resource. Member form, no trailing slash: get /
480
+ * put / delete one chunk by its ordinal index. The `chunkIndex` is a
481
+ * non-negative integer, emitted verbatim (it is never a reserved or dot
482
+ * segment, so it needs no percent-encoding).
329
483
  *
330
484
  * @param options {object}
331
- * @param options.baseUrl {string} the server base URL
485
+ * @param options.spaceUrl {string}
486
+ * @param options.collectionId {string}
487
+ * @param options.resourceId {string}
488
+ * @param options.chunkIndex {number}
332
489
  * @returns {string}
333
490
  */
334
- export function spacesUrl({ baseUrl }) {
335
- return toUrl({ baseUrl, path: '/spaces/' });
491
+ export function resourceChunkUrl({ spaceUrl, collectionId, resourceId, chunkIndex }) {
492
+ return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/chunks/${chunkIndex}`;
336
493
  }
337
494
  /**
338
- * Splits an absolute WAS URL into the server base URL and the path segments
339
- * beneath it. `boundary` picks, from the pathname's segments (split on `/`,
340
- * leading empty segment dropped), the index of the first segment that is not
341
- * part of the base, or `-1` when there is none. The base is everything before
342
- * it, without a trailing slash and in `URL` canonical form (lower-case host,
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.
354
- *
355
- * @param options {object}
356
- * @param options.url {string} an absolute URL
357
- * @param options.boundary {(segments: string[]) => number} the index of the
358
- * first segment beneath the base, or `-1`
359
- * @returns {{ baseUrl: string; rest: string[] } | null} the base URL and
360
- * the segments from the boundary on
361
- */
362
- function splitAtBase({ url, boundary }) {
363
- let parsed;
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;
418
- }
419
- /**
420
- * Resolves a path against the server base URL, producing an absolute URL
421
- * string suitable for zcap `invocationTarget`s.
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.
495
+ * `{spaceUrl}{collectionId}/{resourceId}/chunks/` -- the chunk container of a
496
+ * chunked Resource: its stored chunks, listed. Container form, trailing slash
497
+ * (the member form, {@link resourceChunkUrl}, has none).
426
498
  *
427
499
  * @param options {object}
428
- * @param options.baseUrl {string} the server base URL
429
- * @param options.path {string} a leading-slash path (e.g. `/space/x`)
500
+ * @param options.spaceUrl {string}
501
+ * @param options.collectionId {string}
502
+ * @param options.resourceId {string}
430
503
  * @returns {string}
431
504
  */
432
- export function toUrl({ baseUrl, path }) {
433
- // A leading-slash `path` is origin-absolute to `new URL`, which would drop
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();
505
+ export function resourceChunksUrl({ spaceUrl, collectionId, resourceId }) {
506
+ return `${resourceUrl({ spaceUrl, collectionId, resourceId })}/chunks/`;
439
507
  }
440
508
  /**
441
- * Canonicalizes an absolute collection URL to its items/listing endpoint --
442
- * the trailing-slash form (`/space/:id/:collectionId/`). This module owns the
443
- * trailing-slash rules, so callers that receive a collection URL from outside
444
- * (e.g. a public link) normalize it here rather than re-encoding the rule.
509
+ * The canonical (trailing-slash) form of a container URL received from
510
+ * outside and requested as given: the Spaces Repository URL a service
511
+ * description advertises, a public Collection link. The bare form only
512
+ * 308-redirects, and a signed invocation does not survive the redirect, so
513
+ * the slash is added here before the URL is ever requested. This module owns
514
+ * the trailing-slash rules; callers normalize here rather than re-encoding
515
+ * the rule.
445
516
  *
446
- * @param collectionUrl {string} an absolute collection URL
517
+ * @param url {string} an absolute container URL, in either form
447
518
  * @returns {string}
448
519
  */
449
- export function collectionItemsUrl(collectionUrl) {
450
- return collectionUrl.endsWith('/') ? collectionUrl : `${collectionUrl}/`;
520
+ export function canonicalContainerUrl(url) {
521
+ // The slash marks the end of the path, so it goes before any query or
522
+ // fragment; the rest of the URL is kept as given.
523
+ const pathEnd = url.search(/[?#]/);
524
+ const path = pathEnd === -1 ? url : url.slice(0, pathEnd);
525
+ if (path.endsWith('/')) {
526
+ return url;
527
+ }
528
+ return pathEnd === -1 ? `${url}/` : `${path}/${url.slice(pathEnd)}`;
451
529
  }
452
530
  /**
453
- * Classifies an absolute `target` URL that is expected to live on this server,
454
- * relative to `baseUrl`'s base path -- so a WAS mounted under a sub-path (e.g.
455
- * `https://host/was/`) resolves just as a bare-origin deployment does, which
456
- * `parseSpacePath` alone cannot do (it would see a leading `was` segment).
531
+ * Classifies an absolute `target` URL by where it sits beneath `spaceUrl`:
532
+ * the Space itself, one of its Collections, a Resource, or a reserved
533
+ * sub-endpoint. The inverse of the builders above, kept next to them so the
534
+ * grammar is owned in one place: `WasClient.fromCapability` reads a
535
+ * capability's `invocationTarget` through it, and the builders stay in
536
+ * lockstep.
537
+ *
538
+ * Both URLs are compared in parsed form, so a target serialized by another
539
+ * party (an upper-case host, a default port) still matches. Segments are
540
+ * percent-decoded (the builders re-encode them). Empty segments are dropped,
541
+ * so a container addressed in either form classifies the same: the bare and
542
+ * the trailing-slash form of a Collection are both the Collection. That is
543
+ * deliberate: the builders emit only the canonical form, but the targets
544
+ * parsed here come from capabilities minted elsewhere, and a client that
545
+ * reads only one form would refuse valid ones.
457
546
  *
458
- * Returns `null` when `target` is not beneath `baseUrl` (another origin, or
459
- * another base path), carries a query or fragment, or addresses something
460
- * outside the `/space` tree (e.g. `/kms`). The caller chooses whether that is
461
- * an error.
547
+ * Returns `null` when `target` is not beneath `spaceUrl` (another origin, or
548
+ * another path), carries a query or fragment, or either URL is malformed.
549
+ * The caller chooses whether that is an error.
462
550
  *
463
551
  * @param options {object}
464
- * @param options.baseUrl {string} the client's server base URL
465
- * @param options.target {string} an absolute URL on that server
552
+ * @param options.spaceUrl {string} the Space URL
553
+ * @param options.target {string} an absolute URL expected beneath it
466
554
  * @returns {ParsedSpacePath | null}
467
555
  */
468
- export function parseSpaceTarget({ baseUrl, target }) {
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;
556
+ export function parseSpaceTarget({ spaceUrl, target }) {
473
557
  let base;
474
558
  try {
475
- url = new URL(target);
476
- base = new URL(baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
559
+ base = new URL(canonicalSpaceUrl(spaceUrl));
477
560
  }
478
561
  catch {
479
562
  return null;
480
563
  }
481
- if (url.search !== '' ||
482
- url.hash !== '' ||
483
- url.origin !== base.origin ||
484
- !url.pathname.startsWith(base.pathname)) {
564
+ const url = parseContainerUrl(target);
565
+ if (url === null || url.origin !== base.origin) {
485
566
  return null;
486
567
  }
487
- return parseSpacePath(`/${url.pathname.slice(base.pathname.length)}`);
488
- }
489
- /**
490
- * Classifies an absolute `target` URL whose server is not known in advance:
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) {
568
+ // The bare form of the Space itself (`.../space/s`, no slash) names the
569
+ // Space too, so the prefix check reads the path without its final slash.
570
+ const basePath = base.pathname.slice(0, -1);
571
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
514
572
  return null;
515
573
  }
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
574
  let segments;
541
575
  try {
542
- segments = pathname.split('/').filter(Boolean).map(decodeURIComponent);
576
+ segments = url.pathname
577
+ .slice(basePath.length)
578
+ .split('/')
579
+ .filter(Boolean)
580
+ .map(decodeURIComponent);
543
581
  }
544
582
  catch {
545
583
  // A malformed percent-escape (e.g. `%ff`) makes `decodeURIComponent` throw
546
- // a `URIError`: the path is not a well-formed WAS target, so classify it as
584
+ // a `URIError`: the path is not a well-formed target, so classify it as
547
585
  // unparseable (the caller converts `null` to its own typed error).
548
586
  return null;
549
587
  }
550
- if (segments[0] !== 'space' || segments[1] === undefined) {
551
- return null;
552
- }
553
- const [, spaceId, ...rest] = segments;
554
- if (rest.length === 0) {
555
- return { kind: 'space', spaceId };
588
+ if (segments.length === 0) {
589
+ return { kind: 'space' };
556
590
  }
557
591
  // A reserved segment directly under the space (`policy`, `backends`,
558
592
  // `export`, ...) addresses a space-level sub-endpoint, as does anything
559
- // nested beneath one (`/space/s/backends/:backendId`).
560
- if (isReservedCollectionId(rest[0])) {
561
- return { kind: 'sub-resource', spaceId, segments: rest };
593
+ // nested beneath one (`backends/{backendId}`).
594
+ if (isReservedCollectionId(segments[0])) {
595
+ return { kind: 'sub-resource', segments };
562
596
  }
563
- if (rest.length === 1) {
564
- return { kind: 'collection', spaceId, collectionId: rest[0] };
597
+ if (segments.length === 1) {
598
+ return { kind: 'collection', collectionId: segments[0] };
565
599
  }
566
600
  // A reserved segment under the collection (`policy`, `backend`, `quota`,
567
601
  // ...) addresses a collection-level sub-endpoint.
568
- if (isReservedResourceId(rest[1])) {
569
- return { kind: 'sub-resource', spaceId, segments: rest };
602
+ if (isReservedResourceId(segments[1])) {
603
+ return { kind: 'sub-resource', segments };
570
604
  }
571
- if (rest.length === 2) {
605
+ if (segments.length === 2) {
572
606
  return {
573
607
  kind: 'resource',
574
- spaceId,
575
- collectionId: rest[0],
576
- resourceId: rest[1]
608
+ collectionId: segments[0],
609
+ resourceId: segments[1]
577
610
  };
578
611
  }
579
- // Anything deeper (`/space/s/c/r/meta`, `/space/s/c/r/policy`, chunk
580
- // sub-segments, ...) is a resource-level sub-endpoint.
581
- return { kind: 'sub-resource', spaceId, segments: rest };
612
+ // Anything deeper (`{c}/{r}/meta`, `{c}/{r}/policy`, chunk sub-segments,
613
+ // ...) is a resource-level sub-endpoint.
614
+ return { kind: 'sub-resource', segments };
582
615
  }
583
616
  //# sourceMappingURL=paths.js.map