@interop/was-conformance-suite 0.1.2 → 0.3.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.
- package/README.md +2 -2
- package/dist/suites/authz-ordering-api.d.ts +9 -0
- package/dist/suites/authz-ordering-api.d.ts.map +1 -0
- package/dist/suites/authz-ordering-api.js +286 -0
- package/dist/suites/authz-ordering-api.js.map +1 -0
- package/dist/suites/blinded-index-api.d.ts +11 -0
- package/dist/suites/blinded-index-api.d.ts.map +1 -0
- package/dist/suites/blinded-index-api.js +468 -0
- package/dist/suites/blinded-index-api.js.map +1 -0
- package/dist/suites/changes-query-api.d.ts.map +1 -1
- package/dist/suites/changes-query-api.js +61 -0
- package/dist/suites/changes-query-api.js.map +1 -1
- package/dist/suites/chunks-api.d.ts +27 -0
- package/dist/suites/chunks-api.d.ts.map +1 -0
- package/dist/suites/chunks-api.js +613 -0
- package/dist/suites/chunks-api.js.map +1 -0
- package/dist/suites/client-backends.d.ts.map +1 -1
- package/dist/suites/client-backends.js +23 -0
- package/dist/suites/client-backends.js.map +1 -1
- package/dist/suites/client-delegation.d.ts.map +1 -1
- package/dist/suites/client-delegation.js +12 -0
- package/dist/suites/client-delegation.js.map +1 -1
- package/dist/suites/client-export-import.d.ts.map +1 -1
- package/dist/suites/client-export-import.js +1 -0
- package/dist/suites/client-export-import.js.map +1 -1
- package/dist/suites/client-resources.d.ts.map +1 -1
- package/dist/suites/client-resources.js +30 -0
- package/dist/suites/client-resources.js.map +1 -1
- package/dist/suites/client-spaces.d.ts.map +1 -1
- package/dist/suites/client-spaces.js +27 -0
- package/dist/suites/client-spaces.js.map +1 -1
- package/dist/suites/collection-api.d.ts.map +1 -1
- package/dist/suites/collection-api.js +151 -0
- package/dist/suites/collection-api.js.map +1 -1
- package/dist/suites/conditional-requests-api.d.ts +11 -0
- package/dist/suites/conditional-requests-api.d.ts.map +1 -0
- package/dist/suites/conditional-requests-api.js +444 -0
- package/dist/suites/conditional-requests-api.js.map +1 -0
- package/dist/suites/digest-api.d.ts +7 -0
- package/dist/suites/digest-api.d.ts.map +1 -0
- package/dist/suites/digest-api.js +157 -0
- package/dist/suites/digest-api.js.map +1 -0
- package/dist/suites/encryption-marker-api.d.ts.map +1 -1
- package/dist/suites/encryption-marker-api.js +132 -8
- package/dist/suites/encryption-marker-api.js.map +1 -1
- package/dist/suites/index.d.ts +8 -1
- package/dist/suites/index.d.ts.map +1 -1
- package/dist/suites/index.js +16 -2
- package/dist/suites/index.js.map +1 -1
- package/dist/suites/invocation-target-api.d.ts +12 -0
- package/dist/suites/invocation-target-api.d.ts.map +1 -0
- package/dist/suites/invocation-target-api.js +252 -0
- package/dist/suites/invocation-target-api.js.map +1 -0
- package/dist/suites/policy-api.d.ts.map +1 -1
- package/dist/suites/policy-api.js +23 -0
- package/dist/suites/policy-api.js.map +1 -1
- package/dist/suites/resource-api.d.ts.map +1 -1
- package/dist/suites/resource-api.js +269 -0
- package/dist/suites/resource-api.js.map +1 -1
- package/dist/suites/server.d.ts.map +1 -1
- package/dist/suites/server.js +1 -0
- package/dist/suites/server.js.map +1 -1
- package/dist/suites/spaces-api.d.ts.map +1 -1
- package/dist/suites/spaces-api.js +565 -0
- package/dist/suites/spaces-api.js.map +1 -1
- package/dist/suites/write-validation-api.d.ts +7 -0
- package/dist/suites/write-validation-api.d.ts.map +1 -0
- package/dist/suites/write-validation-api.js +186 -0
- package/dist/suites/write-validation-api.js.map +1 -0
- package/package.json +8 -3
|
@@ -76,6 +76,7 @@ export const spacesApi = {
|
|
|
76
76
|
id: 'repository.anonymous-list-empty',
|
|
77
77
|
name: 'GET /spaces/ without auth headers returns the empty listing (200)',
|
|
78
78
|
group: 'Spaces Repository API',
|
|
79
|
+
specRefs: ['https://wallet.storage/spec#list-spaces-operation'],
|
|
79
80
|
run: async (ctx) => {
|
|
80
81
|
const { serverUrl } = ctx;
|
|
81
82
|
// List Spaces is the spec's exception to 404 masking: an anonymous
|
|
@@ -93,6 +94,7 @@ export const spacesApi = {
|
|
|
93
94
|
id: 'repository.list-scoped-to-controller',
|
|
94
95
|
name: '[root] GET /spaces/ lists only spaces controlled by the requester',
|
|
95
96
|
group: 'Spaces Repository API',
|
|
97
|
+
specRefs: ['https://wallet.storage/spec#list-spaces-operation'],
|
|
96
98
|
run: async (ctx, state) => {
|
|
97
99
|
const { serverUrl, createSpace, generateId } = ctx;
|
|
98
100
|
const { alice, bob } = state;
|
|
@@ -133,6 +135,7 @@ export const spacesApi = {
|
|
|
133
135
|
id: 'repository.create-unauthorized-401',
|
|
134
136
|
name: 'POST /spaces/ should 401 error when no authorization headers',
|
|
135
137
|
group: 'Spaces Repository API',
|
|
138
|
+
specRefs: ['https://wallet.storage/spec#create-space-operation'],
|
|
136
139
|
run: async (ctx) => {
|
|
137
140
|
const { serverUrl } = ctx;
|
|
138
141
|
const response = await fetch(new URL('/spaces/', serverUrl), {
|
|
@@ -142,10 +145,264 @@ export const spacesApi = {
|
|
|
142
145
|
assert.match(response.headers.get('content-type'), /application\/problem\+json/);
|
|
143
146
|
}
|
|
144
147
|
},
|
|
148
|
+
{
|
|
149
|
+
id: 'repository.create-missing-controller-400',
|
|
150
|
+
name: 'POST /spaces/ without a "controller" in the body yields invalid-request-body (400)',
|
|
151
|
+
group: 'Spaces Repository API',
|
|
152
|
+
specRefs: [
|
|
153
|
+
'https://wallet.storage/spec#create-space-errors',
|
|
154
|
+
'https://wallet.storage/spec#invalid-request-body'
|
|
155
|
+
],
|
|
156
|
+
run: async (ctx, state) => {
|
|
157
|
+
const { createSpace, generateId } = ctx;
|
|
158
|
+
const { alice } = state;
|
|
159
|
+
// The `controller` requirement applies before any provisioning or
|
|
160
|
+
// capability concern, so this holds on both the onboarding-token and
|
|
161
|
+
// signed-zcap paths. The token path of createSpace() returns the
|
|
162
|
+
// status; the zcap path throws on non-2xx -- capture either shape.
|
|
163
|
+
let status, problem;
|
|
164
|
+
try {
|
|
165
|
+
const response = await createSpace({
|
|
166
|
+
spaceDescription: {
|
|
167
|
+
id: generateId(),
|
|
168
|
+
name: 'Space With No Controller'
|
|
169
|
+
},
|
|
170
|
+
rootClient: alice.rootClient
|
|
171
|
+
});
|
|
172
|
+
status = response.status;
|
|
173
|
+
problem = response.data;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
status = err.response?.status;
|
|
177
|
+
problem = err.data;
|
|
178
|
+
}
|
|
179
|
+
assert.equal(status, 400);
|
|
180
|
+
assert.equal(problem.type, 'https://wallet.storage/spec#invalid-request-body');
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'repository.create-controller-mismatch-400',
|
|
185
|
+
name: "[root] POST /spaces/ signed by a key that is not the body's controller yields controller-mismatch (400)",
|
|
186
|
+
group: 'Spaces Repository API',
|
|
187
|
+
specRefs: [
|
|
188
|
+
'https://wallet.storage/spec#create-space-errors',
|
|
189
|
+
'https://wallet.storage/spec#controller-mismatch'
|
|
190
|
+
],
|
|
191
|
+
run: async (ctx, state) => {
|
|
192
|
+
const { serverUrl, generateId } = ctx;
|
|
193
|
+
const { alice, bob } = state;
|
|
194
|
+
if (ctx.onboardingToken) {
|
|
195
|
+
// With an onboarding token configured, the token itself vouches for
|
|
196
|
+
// provisioning (delegated provisioning), so the signer-vs-body
|
|
197
|
+
// consent check under test here is legitimately skipped.
|
|
198
|
+
ctx.skip('onboarding token configured: token vouches for provisioning');
|
|
199
|
+
}
|
|
200
|
+
// Create Space is verified against the *body's* controller: Bob signs
|
|
201
|
+
// the invocation but names Alice as controller, with no delegation
|
|
202
|
+
// chain rooted in her -- the direct signer-mismatch shape.
|
|
203
|
+
const spaceId = generateId();
|
|
204
|
+
let expectedError;
|
|
205
|
+
try {
|
|
206
|
+
await bob.rootClient.request({
|
|
207
|
+
url: new URL('/spaces/', serverUrl).toString(),
|
|
208
|
+
method: 'POST',
|
|
209
|
+
action: 'POST',
|
|
210
|
+
json: {
|
|
211
|
+
id: spaceId,
|
|
212
|
+
name: 'Space Bob Claims For Alice',
|
|
213
|
+
controller: alice.did
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
expectedError = err;
|
|
219
|
+
}
|
|
220
|
+
assert.ok(expectedError, 'expected the mismatched create to fail');
|
|
221
|
+
assert.equal(expectedError.response.status, 400);
|
|
222
|
+
assert.equal(expectedError.data.type, 'https://wallet.storage/spec#controller-mismatch');
|
|
223
|
+
// The Space must not have been created: its named controller (Alice)
|
|
224
|
+
// would be able to read it if it had been.
|
|
225
|
+
let checkError;
|
|
226
|
+
try {
|
|
227
|
+
await alice.rootClient.request({
|
|
228
|
+
url: new URL(`/space/${spaceId}`, serverUrl).toString(),
|
|
229
|
+
method: 'GET'
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
checkError = err;
|
|
234
|
+
}
|
|
235
|
+
assert.ok(checkError, 'the rejected Space must not exist');
|
|
236
|
+
assert.equal(checkError.response.status, 404);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'repository.create-invalid-id-400',
|
|
241
|
+
name: 'POST /spaces/ with a non-URL-safe space id yields invalid-id (400)',
|
|
242
|
+
group: 'Spaces Repository API',
|
|
243
|
+
specRefs: [
|
|
244
|
+
'https://wallet.storage/spec#identifiers',
|
|
245
|
+
'https://wallet.storage/spec#invalid-id'
|
|
246
|
+
],
|
|
247
|
+
run: async (ctx, state) => {
|
|
248
|
+
const { createSpace } = ctx;
|
|
249
|
+
const { alice } = state;
|
|
250
|
+
// The id is carried in the request body (not the URL), so it reaches
|
|
251
|
+
// id validation rather than being reshaped by routing. A space
|
|
252
|
+
// character is outside the RFC 3986 unreserved set, so the id is not
|
|
253
|
+
// URL-safe (spec: Identifier Required Properties). Rejection precedes
|
|
254
|
+
// any provisioning concern, so this holds on both the onboarding-token
|
|
255
|
+
// and signed-zcap paths: the token path of createSpace() returns the
|
|
256
|
+
// status; the zcap path throws on non-2xx -- capture either shape.
|
|
257
|
+
let status, problem;
|
|
258
|
+
try {
|
|
259
|
+
const response = await createSpace({
|
|
260
|
+
spaceDescription: {
|
|
261
|
+
id: 'a b',
|
|
262
|
+
name: 'Space With An Unsafe Id',
|
|
263
|
+
controller: alice.did
|
|
264
|
+
},
|
|
265
|
+
rootClient: alice.rootClient
|
|
266
|
+
});
|
|
267
|
+
status = response.status;
|
|
268
|
+
problem = response.data;
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
status = err.response?.status;
|
|
272
|
+
problem = err.data;
|
|
273
|
+
}
|
|
274
|
+
assert.equal(status, 400);
|
|
275
|
+
assert.equal(problem.type, 'https://wallet.storage/spec#invalid-id');
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'repository.create-controller-not-did-400',
|
|
280
|
+
name: 'POST /spaces/ whose body controller is not a DID is rejected (400 invalid-request-body)',
|
|
281
|
+
group: 'Spaces Repository API',
|
|
282
|
+
specRefs: [
|
|
283
|
+
'https://wallet.storage/spec#create-space-errors',
|
|
284
|
+
'https://wallet.storage/spec#invalid-request-body'
|
|
285
|
+
],
|
|
286
|
+
run: async (ctx, state) => {
|
|
287
|
+
const { createSpace, generateId } = ctx;
|
|
288
|
+
const { alice } = state;
|
|
289
|
+
// A `controller` that is present but not a DID (here an https URL) is a
|
|
290
|
+
// malformed request body -- rejected before any provisioning concern,
|
|
291
|
+
// so this holds on both the onboarding-token and signed-zcap paths. The
|
|
292
|
+
// token path of createSpace() returns the status; the zcap path throws
|
|
293
|
+
// on non-2xx -- capture either shape.
|
|
294
|
+
let status, problem;
|
|
295
|
+
try {
|
|
296
|
+
const response = await createSpace({
|
|
297
|
+
spaceDescription: {
|
|
298
|
+
id: generateId(),
|
|
299
|
+
name: 'Space With A Non-DID Controller',
|
|
300
|
+
controller: 'https://example.com/alice'
|
|
301
|
+
},
|
|
302
|
+
rootClient: alice.rootClient
|
|
303
|
+
});
|
|
304
|
+
status = response.status;
|
|
305
|
+
problem = response.data;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
status = err.response?.status;
|
|
309
|
+
problem = err.data;
|
|
310
|
+
}
|
|
311
|
+
assert.equal(status, 400);
|
|
312
|
+
assert.equal(problem.type, 'https://wallet.storage/spec#invalid-request-body');
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'repository.create-delegated-201',
|
|
317
|
+
name: "[delegated] a provisioning app creates a Space on Alice's behalf via POST (201)",
|
|
318
|
+
group: 'Spaces Repository API',
|
|
319
|
+
specRefs: [
|
|
320
|
+
'https://wallet.storage/spec#create-space-operation',
|
|
321
|
+
'https://wallet.storage/spec#was-authorization-profile-v0-1'
|
|
322
|
+
],
|
|
323
|
+
run: async (ctx, state) => {
|
|
324
|
+
const { serverUrl, zcapClient, generateId } = ctx;
|
|
325
|
+
const { alice, aliceDelegatedApp } = state;
|
|
326
|
+
if (ctx.onboardingToken) {
|
|
327
|
+
// With an onboarding token configured, the token vouches for
|
|
328
|
+
// provisioning, so the delegated-consent path under test here is
|
|
329
|
+
// legitimately bypassed.
|
|
330
|
+
ctx.skip('onboarding token configured: token vouches for provisioning');
|
|
331
|
+
}
|
|
332
|
+
// Alice delegates a POST /spaces/ capability to her app; the app
|
|
333
|
+
// invokes it, naming Alice as the controller. The invocation is
|
|
334
|
+
// authorized by a delegation chain rooted in the body's controller, so
|
|
335
|
+
// the create succeeds and Alice controls the Space from the start
|
|
336
|
+
// (delegated provisioning).
|
|
337
|
+
const spacesUrl = new URL('/spaces/', serverUrl).toString();
|
|
338
|
+
const aliceAppClient = zcapClient({ signer: aliceDelegatedApp.signer });
|
|
339
|
+
const provisioningCapability = await alice.rootClient.delegate({
|
|
340
|
+
allowedActions: ['POST'],
|
|
341
|
+
invocationTarget: spacesUrl,
|
|
342
|
+
controller: aliceDelegatedApp.did
|
|
343
|
+
});
|
|
344
|
+
const spaceId = generateId();
|
|
345
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
346
|
+
try {
|
|
347
|
+
const response = await aliceAppClient.request({
|
|
348
|
+
url: spacesUrl,
|
|
349
|
+
capability: provisioningCapability,
|
|
350
|
+
method: 'POST',
|
|
351
|
+
action: 'POST',
|
|
352
|
+
json: {
|
|
353
|
+
id: spaceId,
|
|
354
|
+
name: 'Provisioned for Alice',
|
|
355
|
+
controller: alice.did
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
assert.equal(response.status, 201);
|
|
359
|
+
assert.equal(response.data.controller, alice.did);
|
|
360
|
+
// The Space exists and is controlled by Alice: her root key reads it.
|
|
361
|
+
const checkResponse = await alice.rootClient.request({
|
|
362
|
+
url: spaceUrl,
|
|
363
|
+
method: 'GET',
|
|
364
|
+
action: 'GET'
|
|
365
|
+
});
|
|
366
|
+
assert.equal(checkResponse.status, 200);
|
|
367
|
+
assert.equal(checkResponse.data.controller, alice.did);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
try {
|
|
371
|
+
await alice.rootClient.request({ url: spaceUrl, method: 'DELETE' });
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
/* best-effort cleanup */
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 'repository.error-body-type-and-title',
|
|
381
|
+
name: 'an error response carries both a non-empty `type` and a non-empty `title`',
|
|
382
|
+
group: 'Spaces Repository API',
|
|
383
|
+
specRefs: ['https://wallet.storage/spec#error-handling'],
|
|
384
|
+
run: async (ctx) => {
|
|
385
|
+
const { serverUrl } = ctx;
|
|
386
|
+
// An unauthenticated Create Space is a request/credential failure (401),
|
|
387
|
+
// returned as problem+json. The spec makes `type` and `title` REQUIRED
|
|
388
|
+
// members of every error body.
|
|
389
|
+
const response = await fetch(new URL('/spaces/', serverUrl), {
|
|
390
|
+
method: 'POST'
|
|
391
|
+
});
|
|
392
|
+
assert.equal(response.status, 401);
|
|
393
|
+
assert.match(response.headers.get('content-type'), /application\/problem\+json/);
|
|
394
|
+
const problem = (await response.json());
|
|
395
|
+
assert.equal(typeof problem.type, 'string');
|
|
396
|
+
assert.ok(problem.type.length > 0, 'error `type` must be non-empty');
|
|
397
|
+
assert.equal(typeof problem.title, 'string');
|
|
398
|
+
assert.ok(problem.title.length > 0, 'error `title` must be non-empty');
|
|
399
|
+
}
|
|
400
|
+
},
|
|
145
401
|
{
|
|
146
402
|
id: 'space.create-post',
|
|
147
403
|
name: '[root] create space via POST',
|
|
148
404
|
group: 'Space API',
|
|
405
|
+
specRefs: ['https://wallet.storage/spec#create-space-operation'],
|
|
149
406
|
run: async (ctx, state) => {
|
|
150
407
|
const { serverUrl, createSpace, generateId, withoutCreatedBy } = ctx;
|
|
151
408
|
const { alice } = state;
|
|
@@ -211,6 +468,9 @@ export const spacesApi = {
|
|
|
211
468
|
id: 'space.create-put',
|
|
212
469
|
name: '[root] create space by id via PUT',
|
|
213
470
|
group: 'Space API',
|
|
471
|
+
specRefs: [
|
|
472
|
+
'https://wallet.storage/spec#update-or-create-by-id-space-operation'
|
|
473
|
+
],
|
|
214
474
|
run: async (ctx, state) => {
|
|
215
475
|
const { serverUrl } = ctx;
|
|
216
476
|
const { alice } = state;
|
|
@@ -233,10 +493,81 @@ export const spacesApi = {
|
|
|
233
493
|
assert.equal(checkResponse.status, 200);
|
|
234
494
|
}
|
|
235
495
|
},
|
|
496
|
+
{
|
|
497
|
+
id: 'space.update-controller-swap-404',
|
|
498
|
+
name: "[root] a PUT swapping 'controller', signed by the would-be new controller, yields 404 and does not transfer the Space",
|
|
499
|
+
group: 'Space API',
|
|
500
|
+
specRefs: [
|
|
501
|
+
'https://wallet.storage/spec#update-or-create-by-id-space-operation',
|
|
502
|
+
'https://wallet.storage/spec#not-found'
|
|
503
|
+
],
|
|
504
|
+
run: async (ctx, state) => {
|
|
505
|
+
const { serverUrl, createSpace, generateId } = ctx;
|
|
506
|
+
const { alice, bob } = state;
|
|
507
|
+
// An update MUST be verified against the *stored* controller, not the
|
|
508
|
+
// body's proposed one -- otherwise any caller could seize an existing
|
|
509
|
+
// Space by PUTting its id with themselves as controller. Bob attempts
|
|
510
|
+
// exactly that takeover; as an unauthorized write to an existing
|
|
511
|
+
// Space, it gets the privacy-merged not-found (404) mask.
|
|
512
|
+
const spaceId = generateId();
|
|
513
|
+
await createSpace({
|
|
514
|
+
spaceDescription: {
|
|
515
|
+
id: spaceId,
|
|
516
|
+
name: 'Escalation Target Space',
|
|
517
|
+
controller: alice.did
|
|
518
|
+
},
|
|
519
|
+
rootClient: alice.rootClient
|
|
520
|
+
});
|
|
521
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
522
|
+
try {
|
|
523
|
+
let expectedError;
|
|
524
|
+
try {
|
|
525
|
+
await bob.rootClient.request({
|
|
526
|
+
url: spaceUrl,
|
|
527
|
+
method: 'PUT',
|
|
528
|
+
action: 'PUT',
|
|
529
|
+
json: {
|
|
530
|
+
id: spaceId,
|
|
531
|
+
name: 'Seized Space',
|
|
532
|
+
controller: bob.did
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
expectedError = err;
|
|
538
|
+
}
|
|
539
|
+
assert.ok(expectedError, 'expected the takeover PUT to fail');
|
|
540
|
+
assert.equal(expectedError.response.status, 404);
|
|
541
|
+
assert.match(expectedError.response.headers.get('content-type'), /application\/problem\+json/);
|
|
542
|
+
assert.equal(expectedError.data.type, 'https://wallet.storage/spec#not-found');
|
|
543
|
+
// The operation must not have been performed: Alice still controls
|
|
544
|
+
// the Space, name and controller unchanged.
|
|
545
|
+
const checkResponse = await alice.rootClient.request({
|
|
546
|
+
url: spaceUrl,
|
|
547
|
+
method: 'GET'
|
|
548
|
+
});
|
|
549
|
+
assert.equal(checkResponse.status, 200);
|
|
550
|
+
assert.equal(checkResponse.data.controller, alice.did);
|
|
551
|
+
assert.equal(checkResponse.data.name, 'Escalation Target Space');
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
try {
|
|
555
|
+
await alice.rootClient.request({ url: spaceUrl, method: 'DELETE' });
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
/* best-effort cleanup */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
236
563
|
{
|
|
237
564
|
id: 'space.anonymous-read-404',
|
|
238
565
|
name: 'GET a space with no auth headers falls through to policy and 404s (no public policy)',
|
|
239
566
|
group: 'Space API',
|
|
567
|
+
specRefs: [
|
|
568
|
+
'https://wallet.storage/spec#read-space-operation',
|
|
569
|
+
'https://wallet.storage/spec#read-space-errors'
|
|
570
|
+
],
|
|
240
571
|
run: async (ctx, state) => {
|
|
241
572
|
const { serverUrl } = ctx;
|
|
242
573
|
const { alice } = state;
|
|
@@ -252,6 +583,10 @@ export const spacesApi = {
|
|
|
252
583
|
id: 'space.read-missing-404',
|
|
253
584
|
name: 'GET /space/:spaceId should 404 error on not found space id',
|
|
254
585
|
group: 'Space API',
|
|
586
|
+
specRefs: [
|
|
587
|
+
'https://wallet.storage/spec#read-space-operation',
|
|
588
|
+
'https://wallet.storage/spec#read-space-errors'
|
|
589
|
+
],
|
|
255
590
|
run: async (ctx, state) => {
|
|
256
591
|
const { serverUrl } = ctx;
|
|
257
592
|
const { alice } = state;
|
|
@@ -275,6 +610,7 @@ export const spacesApi = {
|
|
|
275
610
|
id: 'space.read-authorized',
|
|
276
611
|
name: '[root] read space via GET with proper authorization',
|
|
277
612
|
group: 'Space API',
|
|
613
|
+
specRefs: ['https://wallet.storage/spec#read-space-operation'],
|
|
278
614
|
run: async (ctx, state) => {
|
|
279
615
|
const { serverUrl, withoutCreatedBy } = ctx;
|
|
280
616
|
const { alice } = state;
|
|
@@ -300,6 +636,10 @@ export const spacesApi = {
|
|
|
300
636
|
id: 'space.read-delegated',
|
|
301
637
|
name: '[delegated] authorized app should GET /space/:spaceId',
|
|
302
638
|
group: 'Space API',
|
|
639
|
+
specRefs: [
|
|
640
|
+
'https://wallet.storage/spec#read-space-operation',
|
|
641
|
+
'https://wallet.storage/spec#was-authorization-profile-v0-1'
|
|
642
|
+
],
|
|
303
643
|
run: async (ctx, state) => {
|
|
304
644
|
const { serverUrl, zcapClient, withoutCreatedBy } = ctx;
|
|
305
645
|
const { alice, aliceDelegatedApp } = state;
|
|
@@ -332,6 +672,10 @@ export const spacesApi = {
|
|
|
332
672
|
id: 'space.cross-user-read-404',
|
|
333
673
|
name: '[root] Bob should not be able to GET Alice space',
|
|
334
674
|
group: 'Space API',
|
|
675
|
+
specRefs: [
|
|
676
|
+
'https://wallet.storage/spec#read-space-operation',
|
|
677
|
+
'https://wallet.storage/spec#read-space-errors'
|
|
678
|
+
],
|
|
335
679
|
run: async (ctx, state) => {
|
|
336
680
|
const { serverUrl } = ctx;
|
|
337
681
|
const { alice, bob } = state;
|
|
@@ -352,6 +696,7 @@ export const spacesApi = {
|
|
|
352
696
|
id: 'space.delete',
|
|
353
697
|
name: '[root] Alice should be able to DELETE her provisioned space',
|
|
354
698
|
group: 'Space API',
|
|
699
|
+
specRefs: ['https://wallet.storage/spec#delete-space-operation'],
|
|
355
700
|
run: async (ctx, state) => {
|
|
356
701
|
const { serverUrl, createSpace, generateId } = ctx;
|
|
357
702
|
const { alice } = state;
|
|
@@ -380,10 +725,230 @@ export const spacesApi = {
|
|
|
380
725
|
assert.equal(checkResponse.status, 404);
|
|
381
726
|
}
|
|
382
727
|
},
|
|
728
|
+
{
|
|
729
|
+
id: 'space.create-post-conflict-preserves-original',
|
|
730
|
+
name: '[root] a conflicting POST leaves the original Space untouched (409)',
|
|
731
|
+
group: 'Space API',
|
|
732
|
+
specRefs: [
|
|
733
|
+
'https://wallet.storage/spec#create-space-errors',
|
|
734
|
+
'https://wallet.storage/spec#id-conflict'
|
|
735
|
+
],
|
|
736
|
+
run: async (ctx, state) => {
|
|
737
|
+
const { serverUrl, createSpace, generateId } = ctx;
|
|
738
|
+
const { alice, bob } = state;
|
|
739
|
+
const spaceId = generateId();
|
|
740
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
741
|
+
await createSpace({
|
|
742
|
+
spaceDescription: {
|
|
743
|
+
id: spaceId,
|
|
744
|
+
name: 'Original Space',
|
|
745
|
+
controller: alice.did
|
|
746
|
+
},
|
|
747
|
+
rootClient: alice.rootClient
|
|
748
|
+
});
|
|
749
|
+
try {
|
|
750
|
+
// POST the same id again with a different name and controller. The
|
|
751
|
+
// existence check precedes any provisioning concern, so this is a 409
|
|
752
|
+
// id-conflict; create-or-replace by id is PUT's job. The token path
|
|
753
|
+
// of createSpace() returns the status; the zcap path throws on
|
|
754
|
+
// non-2xx -- capture either shape.
|
|
755
|
+
let status, problem;
|
|
756
|
+
try {
|
|
757
|
+
const response = await createSpace({
|
|
758
|
+
spaceDescription: {
|
|
759
|
+
id: spaceId,
|
|
760
|
+
name: 'Usurping Space',
|
|
761
|
+
controller: bob.did
|
|
762
|
+
},
|
|
763
|
+
rootClient: alice.rootClient
|
|
764
|
+
});
|
|
765
|
+
status = response.status;
|
|
766
|
+
problem = response.data;
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
status = err.response?.status;
|
|
770
|
+
problem = err.data;
|
|
771
|
+
}
|
|
772
|
+
assert.equal(status, 409);
|
|
773
|
+
assert.equal(problem.type, 'https://wallet.storage/spec#id-conflict');
|
|
774
|
+
// The original Space is untouched: its name and controller are as
|
|
775
|
+
// first created, not the conflicting POST's proposed values.
|
|
776
|
+
const checkResponse = await alice.rootClient.request({
|
|
777
|
+
url: spaceUrl,
|
|
778
|
+
method: 'GET',
|
|
779
|
+
action: 'GET'
|
|
780
|
+
});
|
|
781
|
+
assert.equal(checkResponse.status, 200);
|
|
782
|
+
assert.equal(checkResponse.data.name, 'Original Space');
|
|
783
|
+
assert.equal(checkResponse.data.controller, alice.did);
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
try {
|
|
787
|
+
await alice.rootClient.request({ url: spaceUrl, method: 'DELETE' });
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
/* best-effort cleanup */
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
id: 'space.create-ignores-body-createdby',
|
|
797
|
+
name: '[root] a body-supplied `createdBy` is ignored by the server',
|
|
798
|
+
group: 'Space API',
|
|
799
|
+
specRefs: ['https://wallet.storage/spec#space-data-model'],
|
|
800
|
+
run: async (ctx, state) => {
|
|
801
|
+
const { serverUrl, createSpace, generateId } = ctx;
|
|
802
|
+
const { alice } = state;
|
|
803
|
+
// `createdBy` is server-managed and read-only: the spec requires a
|
|
804
|
+
// server to ignore a `createdBy` supplied in a request body. The value
|
|
805
|
+
// below is a well-formed but bogus DID that is not the creator.
|
|
806
|
+
const bogusCreatedBy = 'did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJXXXX';
|
|
807
|
+
const spaceId = generateId();
|
|
808
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
809
|
+
await createSpace({
|
|
810
|
+
spaceDescription: {
|
|
811
|
+
id: spaceId,
|
|
812
|
+
name: 'Space With A Bogus createdBy',
|
|
813
|
+
controller: alice.did,
|
|
814
|
+
createdBy: bogusCreatedBy
|
|
815
|
+
},
|
|
816
|
+
rootClient: alice.rootClient
|
|
817
|
+
});
|
|
818
|
+
try {
|
|
819
|
+
const response = await alice.rootClient.request({
|
|
820
|
+
url: spaceUrl,
|
|
821
|
+
method: 'GET',
|
|
822
|
+
action: 'GET'
|
|
823
|
+
});
|
|
824
|
+
assert.equal(response.status, 200);
|
|
825
|
+
// Either absent (token-provisioned create records no createdBy) or
|
|
826
|
+
// set to the real invoker -- never the bogus body value.
|
|
827
|
+
assert.notEqual(response.data.createdBy, bogusCreatedBy, 'the server must ignore a body-supplied createdBy');
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
try {
|
|
831
|
+
await alice.rootClient.request({ url: spaceUrl, method: 'DELETE' });
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
/* best-effort cleanup */
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
id: 'space.create-put-controller-mismatch-400',
|
|
841
|
+
name: '[root] PUT-creating a new Space, signed by someone other than the body controller, yields controller-mismatch (400)',
|
|
842
|
+
group: 'Space API',
|
|
843
|
+
specRefs: [
|
|
844
|
+
'https://wallet.storage/spec#update-or-create-by-id-space-operation',
|
|
845
|
+
'https://wallet.storage/spec#controller-mismatch'
|
|
846
|
+
],
|
|
847
|
+
run: async (ctx, state) => {
|
|
848
|
+
const { serverUrl, generateId } = ctx;
|
|
849
|
+
const { alice, bob } = state;
|
|
850
|
+
// The create branch of PUT is authorized by the *body's* controller,
|
|
851
|
+
// just like Create Space via POST. Bob signs a PUT that would create a
|
|
852
|
+
// new Space naming Alice as controller, with no delegation chain rooted
|
|
853
|
+
// in her -- the direct signer-mismatch shape (this is the CREATE
|
|
854
|
+
// branch; the update branch is covered by space.update-controller-swap).
|
|
855
|
+
const spaceId = generateId();
|
|
856
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
857
|
+
let expectedError;
|
|
858
|
+
try {
|
|
859
|
+
await bob.rootClient.request({
|
|
860
|
+
url: spaceUrl,
|
|
861
|
+
method: 'PUT',
|
|
862
|
+
action: 'PUT',
|
|
863
|
+
json: {
|
|
864
|
+
id: spaceId,
|
|
865
|
+
name: 'Consentless Space',
|
|
866
|
+
controller: alice.did
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
expectedError = err;
|
|
872
|
+
}
|
|
873
|
+
assert.ok(expectedError, 'expected the unconsented PUT-create to fail');
|
|
874
|
+
assert.equal(expectedError.response.status, 400);
|
|
875
|
+
assert.equal(expectedError.data.type, 'https://wallet.storage/spec#controller-mismatch');
|
|
876
|
+
// The Space must not have been created: its named controller (Alice)
|
|
877
|
+
// would be able to read it if it had been.
|
|
878
|
+
let checkError;
|
|
879
|
+
try {
|
|
880
|
+
await alice.rootClient.request({
|
|
881
|
+
url: spaceUrl,
|
|
882
|
+
method: 'GET',
|
|
883
|
+
action: 'GET'
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
checkError = err;
|
|
888
|
+
}
|
|
889
|
+
assert.ok(checkError, 'the rejected Space must not exist');
|
|
890
|
+
assert.equal(checkError.response.status, 404);
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
id: 'space.reserved-policy-segment',
|
|
895
|
+
name: "[root] GET /space/:spaceId/policy is served as the policy endpoint, not a collection named 'policy'",
|
|
896
|
+
group: 'Space API',
|
|
897
|
+
specRefs: [
|
|
898
|
+
'https://wallet.storage/spec#reserved-path-segment-registry',
|
|
899
|
+
'https://wallet.storage/spec#space-level-reserved-endpoints'
|
|
900
|
+
],
|
|
901
|
+
run: async (ctx, state) => {
|
|
902
|
+
const { serverUrl, createSpace, generateId } = ctx;
|
|
903
|
+
const { alice } = state;
|
|
904
|
+
const spaceId = generateId();
|
|
905
|
+
const spaceUrl = new URL(`/space/${spaceId}`, serverUrl).toString();
|
|
906
|
+
const policyUrl = new URL(`/space/${spaceId}/policy`, serverUrl).toString();
|
|
907
|
+
await createSpace({
|
|
908
|
+
spaceDescription: {
|
|
909
|
+
id: spaceId,
|
|
910
|
+
name: 'Reserved Segment Space',
|
|
911
|
+
controller: alice.did
|
|
912
|
+
},
|
|
913
|
+
rootClient: alice.rootClient
|
|
914
|
+
});
|
|
915
|
+
try {
|
|
916
|
+
// `policy` is a reserved space-level segment: it addresses the
|
|
917
|
+
// access-control policy resource, and MUST NOT be repurposed as a
|
|
918
|
+
// Collection id. Set a policy, then read it back through the same
|
|
919
|
+
// segment to confirm it is served as the policy endpoint.
|
|
920
|
+
await alice.rootClient.request({
|
|
921
|
+
url: policyUrl,
|
|
922
|
+
method: 'PUT',
|
|
923
|
+
action: 'PUT',
|
|
924
|
+
json: { type: 'PublicCanRead' }
|
|
925
|
+
});
|
|
926
|
+
const response = await alice.rootClient.request({
|
|
927
|
+
url: policyUrl,
|
|
928
|
+
method: 'GET',
|
|
929
|
+
action: 'GET'
|
|
930
|
+
});
|
|
931
|
+
assert.equal(response.status, 200);
|
|
932
|
+
// The response is a policy document -- it carries the policy `type`
|
|
933
|
+
// we stored -- not a Collection listing (which would carry `items`).
|
|
934
|
+
assert.equal(response.data.type, 'PublicCanRead');
|
|
935
|
+
assert.equal(response.data.items, undefined, 'the policy endpoint must not return a Collection listing');
|
|
936
|
+
}
|
|
937
|
+
finally {
|
|
938
|
+
try {
|
|
939
|
+
await alice.rootClient.request({ url: spaceUrl, method: 'DELETE' });
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
/* best-effort cleanup */
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
},
|
|
383
947
|
{
|
|
384
948
|
id: 'collections.list-for-space',
|
|
385
949
|
name: '[root] GET /space/:spaceId/collections/ lists collections for a space',
|
|
386
950
|
group: 'Collections API',
|
|
951
|
+
specRefs: ['https://wallet.storage/spec#list-all-collections-operation'],
|
|
387
952
|
run: async (ctx, state) => {
|
|
388
953
|
const { serverUrl } = ctx;
|
|
389
954
|
const { alice, collectionId } = state;
|