was-teaching-server 0.9.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/CHANGELOG.md +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handlers for Space operations: get/update/delete a Space, add a
|
|
3
|
+
* Collection to it, list its Collections, and export it.
|
|
4
|
+
*/
|
|
5
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
6
|
+
import type { Readable } from 'node:stream'
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
8
|
+
import { handleZcapVerify } from '../zcap.js'
|
|
9
|
+
import { buildLinkset } from '../policy.js'
|
|
10
|
+
import {
|
|
11
|
+
fetchSpaceAndAuthorize,
|
|
12
|
+
fetchSpaceAndVerify,
|
|
13
|
+
invalidateSpaceDescription
|
|
14
|
+
} from './spaceContext.js'
|
|
15
|
+
import { verifyBodyControllerConsent } from './controllerConsent.js'
|
|
16
|
+
import { invokerDid } from '../auth-header-hooks.js'
|
|
17
|
+
import { assertValidIds, assertValidId } from '../lib/validateId.js'
|
|
18
|
+
import { assertValidController } from '../lib/validateDid.js'
|
|
19
|
+
import {
|
|
20
|
+
assertSupportedBackend,
|
|
21
|
+
listRegisteredBackends
|
|
22
|
+
} from '../lib/backends.js'
|
|
23
|
+
import { assertSupportedEncryption } from '../lib/encryption.js'
|
|
24
|
+
import { formatEtag } from '../lib/etag.js'
|
|
25
|
+
import {
|
|
26
|
+
spacePath,
|
|
27
|
+
collectionPath,
|
|
28
|
+
collectionsPath,
|
|
29
|
+
exportPath,
|
|
30
|
+
importPath,
|
|
31
|
+
linksetPath,
|
|
32
|
+
backendsPath,
|
|
33
|
+
quotasPath
|
|
34
|
+
} from '../lib/paths.js'
|
|
35
|
+
import {
|
|
36
|
+
ProblemError,
|
|
37
|
+
InvalidSpaceIdError,
|
|
38
|
+
InvalidImportError,
|
|
39
|
+
InvalidRequestBodyError,
|
|
40
|
+
IdConflictError,
|
|
41
|
+
SpaceControllerMismatchError
|
|
42
|
+
} from '../errors.js'
|
|
43
|
+
import type {
|
|
44
|
+
IDID,
|
|
45
|
+
SpaceDescription,
|
|
46
|
+
CollectionsList,
|
|
47
|
+
SpaceQuotaReport
|
|
48
|
+
} from '../types.js'
|
|
49
|
+
|
|
50
|
+
export class SpaceRequest {
|
|
51
|
+
/**
|
|
52
|
+
* GET /space/:spaceId
|
|
53
|
+
* Request handler for "Read Space" request
|
|
54
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
55
|
+
* request.zcap: {
|
|
56
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* Example Space Description Object:
|
|
60
|
+
* {
|
|
61
|
+
* "id": "6b5be748-5f39-4936-a895-409e393c399c",
|
|
62
|
+
* "type": ["Space"],
|
|
63
|
+
* "name": "Alice's space",
|
|
64
|
+
* "controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* @param request {import('fastify').FastifyRequest}
|
|
68
|
+
* @param reply {import('fastify').FastifyReply}
|
|
69
|
+
* @returns {Promise<FastifyReply>}
|
|
70
|
+
*/
|
|
71
|
+
static async get(
|
|
72
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
73
|
+
reply: FastifyReply
|
|
74
|
+
): Promise<FastifyReply> {
|
|
75
|
+
const {
|
|
76
|
+
params: { spaceId }
|
|
77
|
+
} = request
|
|
78
|
+
const requestName = 'Get Space'
|
|
79
|
+
|
|
80
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
81
|
+
assertValidIds({ spaceId }, { requestName })
|
|
82
|
+
|
|
83
|
+
// Authorize (capability-or-policy): capability invocation first, then the
|
|
84
|
+
// Space's access-control policy as a fallback (a public-readable Space).
|
|
85
|
+
const { spaceDescription } = await fetchSpaceAndAuthorize({
|
|
86
|
+
request,
|
|
87
|
+
spaceId,
|
|
88
|
+
targetPath: spacePath({ spaceId }),
|
|
89
|
+
requestName
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// authorized, continue. Advertise the Space's self `url` and linkset (policy
|
|
93
|
+
// discovery); both relative, consistent with the other URL fields the API
|
|
94
|
+
// returns. `type` is served lexically sorted (spec SHOULD).
|
|
95
|
+
const url = spacePath({ spaceId })
|
|
96
|
+
const linkset = linksetPath({ spaceId })
|
|
97
|
+
return reply.status(200).send({
|
|
98
|
+
...spaceDescription,
|
|
99
|
+
type: [...spaceDescription.type].sort(),
|
|
100
|
+
url,
|
|
101
|
+
linkset
|
|
102
|
+
} satisfies SpaceDescription)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* GET /space/:spaceId/linkset
|
|
107
|
+
* Request handler for the Space's linkset (RFC9264): advertises the Space's
|
|
108
|
+
* access-control `policy` resource for discovery. Readable by whoever may read
|
|
109
|
+
* the Space (capability or fallback policy).
|
|
110
|
+
*
|
|
111
|
+
* @param request {import('fastify').FastifyRequest}
|
|
112
|
+
* @param reply {import('fastify').FastifyReply}
|
|
113
|
+
* @returns {Promise<FastifyReply>}
|
|
114
|
+
*/
|
|
115
|
+
static async linkset(
|
|
116
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
117
|
+
reply: FastifyReply
|
|
118
|
+
): Promise<FastifyReply> {
|
|
119
|
+
const {
|
|
120
|
+
params: { spaceId }
|
|
121
|
+
} = request
|
|
122
|
+
const { storage } = request.server
|
|
123
|
+
const requestName = 'Get Space Linkset'
|
|
124
|
+
|
|
125
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
126
|
+
assertValidIds({ spaceId }, { requestName })
|
|
127
|
+
|
|
128
|
+
// Authorize (capability-or-policy): readable by whoever may read the Space.
|
|
129
|
+
await fetchSpaceAndAuthorize({
|
|
130
|
+
request,
|
|
131
|
+
spaceId,
|
|
132
|
+
targetPath: linksetPath({ spaceId }),
|
|
133
|
+
requestName
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const linkset = await buildLinkset({ storage, spaceId })
|
|
137
|
+
return reply
|
|
138
|
+
.status(200)
|
|
139
|
+
.type('application/linkset+json')
|
|
140
|
+
.send(JSON.stringify(linkset))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* PUT /space/:spaceId
|
|
145
|
+
* Request handler for "Update or Create Space by Id" request
|
|
146
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
147
|
+
* request.zcap: {
|
|
148
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
149
|
+
* }
|
|
150
|
+
*
|
|
151
|
+
* @param request {import('fastify').FastifyRequest}
|
|
152
|
+
* @param reply {import('fastify').FastifyReply}
|
|
153
|
+
* @returns {Promise<FastifyReply>}
|
|
154
|
+
*/
|
|
155
|
+
static async put(
|
|
156
|
+
request: FastifyRequest<{
|
|
157
|
+
Params: { spaceId: string }
|
|
158
|
+
Body: { id?: string; name?: string; controller: IDID }
|
|
159
|
+
}>,
|
|
160
|
+
reply: FastifyReply
|
|
161
|
+
): Promise<FastifyReply> {
|
|
162
|
+
const {
|
|
163
|
+
params: { spaceId },
|
|
164
|
+
url,
|
|
165
|
+
method,
|
|
166
|
+
headers,
|
|
167
|
+
body
|
|
168
|
+
} = request
|
|
169
|
+
const { serverUrl, storage } = request.server
|
|
170
|
+
// PUT is a non-safe method, so `requireAuthHeaders` guarantees auth headers
|
|
171
|
+
// were present and `parseAuthHeaders` set `request.zcap` before this handler.
|
|
172
|
+
const { keyId } = request.zcap!
|
|
173
|
+
|
|
174
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
175
|
+
assertValidIds({ spaceId }, { requestName: 'Update Space' })
|
|
176
|
+
|
|
177
|
+
// The Space `id` is immutable: when the PUT body carries one, it must match
|
|
178
|
+
// the `{space_id}` in the URL (spec: Update Space `invalid-request-body`).
|
|
179
|
+
if (body?.id !== undefined && body.id !== spaceId) {
|
|
180
|
+
throw new InvalidRequestBodyError({
|
|
181
|
+
requestName: 'Update Space',
|
|
182
|
+
detail: `Space Description "id" (${body.id}) does not match the URL Space id (${spaceId}).`,
|
|
183
|
+
pointer: '#/id'
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The Space Description body must carry a controller DID. The `name`
|
|
188
|
+
// property is optional (see spec: Space Description object).
|
|
189
|
+
if (!body?.controller) {
|
|
190
|
+
throw new InvalidRequestBodyError({
|
|
191
|
+
requestName: 'Update Space',
|
|
192
|
+
detail: 'Space Description body requires a "controller" property.',
|
|
193
|
+
pointer: '#/controller'
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
// Reject a malformed / non-`did:key` controller before it is stored.
|
|
197
|
+
assertValidController(body.controller, { requestName: 'Update Space' })
|
|
198
|
+
|
|
199
|
+
// Check to see if space already exists (if yes, this will be an Update)
|
|
200
|
+
const existingSpaceDescription = await storage.getSpaceDescription({
|
|
201
|
+
spaceId
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const [zcapSigningDid] = keyId.split('#')
|
|
205
|
+
|
|
206
|
+
request.log.info(
|
|
207
|
+
`Handling PUT request for spaceId: ${spaceId}, zcapSigningDid: ${zcapSigningDid}, existingSpaceDescription: ${existingSpaceDescription ? 'exists' : 'does not exist'}`
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// Perform zCap signature verification (throws appropriate errors)
|
|
211
|
+
let spaceUrl
|
|
212
|
+
try {
|
|
213
|
+
spaceUrl = new URL(spacePath({ spaceId }), serverUrl).toString()
|
|
214
|
+
} catch (err) {
|
|
215
|
+
request.log.error(
|
|
216
|
+
`Failed to construct spaceUrl for spaceId: ${spaceId}, serverUrl: ${serverUrl}, error: ${(err as Error).message}`
|
|
217
|
+
)
|
|
218
|
+
throw new InvalidSpaceIdError({ requestName: 'Update Space' })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
request.log.info(`spaceUrl: ${spaceUrl}, serverUrl: ${serverUrl}`)
|
|
222
|
+
// Important. For existing Spaces, the request must carry authorization
|
|
223
|
+
// matching the *stored* controller (the body's controller is just the
|
|
224
|
+
// proposed new value). On create there is no stored controller yet, so --
|
|
225
|
+
// as with Create Space via POST -- the invocation must be authorized by
|
|
226
|
+
// the *body's* controller: signed directly by it, or via a delegation
|
|
227
|
+
// chain rooted in it (see `verifyBodyControllerConsent`).
|
|
228
|
+
if (existingSpaceDescription) {
|
|
229
|
+
await handleZcapVerify({
|
|
230
|
+
url,
|
|
231
|
+
allowedTarget: spaceUrl,
|
|
232
|
+
allowedAction: 'PUT',
|
|
233
|
+
method,
|
|
234
|
+
headers,
|
|
235
|
+
serverUrl,
|
|
236
|
+
spaceController: existingSpaceDescription.controller,
|
|
237
|
+
logger: request.log,
|
|
238
|
+
revocation: { storage, scope: { spaceId } }
|
|
239
|
+
})
|
|
240
|
+
} else {
|
|
241
|
+
await verifyBodyControllerConsent({
|
|
242
|
+
request,
|
|
243
|
+
controller: body.controller,
|
|
244
|
+
allowedTarget: spaceUrl,
|
|
245
|
+
allowedAction: 'PUT',
|
|
246
|
+
MismatchError: SpaceControllerMismatchError
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
request.log.info('zCap verified')
|
|
251
|
+
|
|
252
|
+
// Compose Space Description object body, new or updated. `name` is
|
|
253
|
+
// optional, so only include it when the request supplies one.
|
|
254
|
+
const spaceDescription = existingSpaceDescription
|
|
255
|
+
? // Existing: Update only the allowed fields
|
|
256
|
+
{
|
|
257
|
+
...existingSpaceDescription,
|
|
258
|
+
id: spaceId,
|
|
259
|
+
controller: body.controller,
|
|
260
|
+
...(body.name !== undefined && { name: body.name })
|
|
261
|
+
}
|
|
262
|
+
: // New Space
|
|
263
|
+
{
|
|
264
|
+
id: spaceId,
|
|
265
|
+
type: ['Space'],
|
|
266
|
+
controller: body.controller,
|
|
267
|
+
...(body.name !== undefined && { name: body.name })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// zCap checks out, continue
|
|
271
|
+
await storage.writeSpace({
|
|
272
|
+
spaceId,
|
|
273
|
+
spaceDescription,
|
|
274
|
+
createdBy: invokerDid(request)
|
|
275
|
+
})
|
|
276
|
+
// Bust any cached (now-stale) description so the next read sees this write.
|
|
277
|
+
invalidateSpaceDescription({ storage, spaceId })
|
|
278
|
+
|
|
279
|
+
reply.header('Location', spaceUrl)
|
|
280
|
+
return existingSpaceDescription
|
|
281
|
+
? reply.status(204).send() // update
|
|
282
|
+
: reply.status(201).send(spaceDescription) // create
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* POST /space/:spaceId/
|
|
287
|
+
* Request handler for "Create Collection" request
|
|
288
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
289
|
+
* request.zcap: {
|
|
290
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
291
|
+
* }
|
|
292
|
+
*
|
|
293
|
+
* @param request {import('fastify').FastifyRequest}
|
|
294
|
+
* @param reply {import('fastify').FastifyReply}
|
|
295
|
+
* @returns {Promise<FastifyReply>}
|
|
296
|
+
*/
|
|
297
|
+
static async post(
|
|
298
|
+
request: FastifyRequest<{
|
|
299
|
+
Params: { spaceId: string }
|
|
300
|
+
Body: {
|
|
301
|
+
id?: string
|
|
302
|
+
name?: string
|
|
303
|
+
backend?: unknown
|
|
304
|
+
encryption?: unknown
|
|
305
|
+
}
|
|
306
|
+
}>,
|
|
307
|
+
reply: FastifyReply
|
|
308
|
+
): Promise<FastifyReply> {
|
|
309
|
+
const {
|
|
310
|
+
params: { spaceId },
|
|
311
|
+
body
|
|
312
|
+
} = request
|
|
313
|
+
const { serverUrl, storage } = request.server
|
|
314
|
+
const requestName = 'Create Collection'
|
|
315
|
+
|
|
316
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
317
|
+
assertValidIds({ spaceId }, { requestName })
|
|
318
|
+
if (body?.id !== undefined) {
|
|
319
|
+
assertValidId(body.id, { kind: 'collection', requestName })
|
|
320
|
+
}
|
|
321
|
+
// Validate the optional client-side encryption marker (shape only; the
|
|
322
|
+
// server stores it opaquely and never decrypts). Absent => plaintext.
|
|
323
|
+
const encryption = assertSupportedEncryption({
|
|
324
|
+
encryption: body?.encryption,
|
|
325
|
+
requestName
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// Verify (capability-only): creating a Collection requires a valid
|
|
329
|
+
// capability invocation; no access-control-policy fallback.
|
|
330
|
+
await fetchSpaceAndVerify({
|
|
331
|
+
request,
|
|
332
|
+
spaceId,
|
|
333
|
+
targetPath: spacePath({ spaceId, trailingSlash: true }),
|
|
334
|
+
requestName
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// zCap checks out, continue.
|
|
338
|
+
// Validate (and default-fill) the selected backend against the Space's
|
|
339
|
+
// backends-available: a bad shape is 400, an unknown id is 409. Checked
|
|
340
|
+
// AFTER verification (it reads the Space's registered backend ids) so an
|
|
341
|
+
// unauthorized caller cannot enumerate registered ids by distinguishing a
|
|
342
|
+
// 409 from the masked 404 -- like the `id-conflict` check below.
|
|
343
|
+
const backend = await assertSupportedBackend({
|
|
344
|
+
storage,
|
|
345
|
+
spaceId,
|
|
346
|
+
backend: body?.backend,
|
|
347
|
+
requestName
|
|
348
|
+
})
|
|
349
|
+
// POST must not replace an existing Collection: spec `id-conflict` (409);
|
|
350
|
+
// create-or-replace by id is PUT's job. Checked after the capability
|
|
351
|
+
// verification so an unauthorized caller cannot probe Collection ids.
|
|
352
|
+
if (
|
|
353
|
+
body?.id !== undefined &&
|
|
354
|
+
(await storage.getCollectionDescription({
|
|
355
|
+
spaceId,
|
|
356
|
+
collectionId: body.id
|
|
357
|
+
}))
|
|
358
|
+
) {
|
|
359
|
+
throw new IdConflictError({ kind: 'Collection' })
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// TODO: Protect against .space resource id collision
|
|
363
|
+
const collectionId = body.id || uuidv4()
|
|
364
|
+
// `name` is optional; default it to the Collection id when missing (spec).
|
|
365
|
+
const name = body.name ?? collectionId
|
|
366
|
+
const collectionDescription = {
|
|
367
|
+
id: collectionId,
|
|
368
|
+
type: ['Collection'],
|
|
369
|
+
name,
|
|
370
|
+
backend,
|
|
371
|
+
...(encryption !== undefined && { encryption })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const createdBy = invokerDid(request)
|
|
375
|
+
const written = await storage.writeCollection({
|
|
376
|
+
spaceId,
|
|
377
|
+
collectionId,
|
|
378
|
+
collectionDescription,
|
|
379
|
+
createdBy
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const createdUrl = new URL(
|
|
383
|
+
collectionPath({ spaceId, collectionId }),
|
|
384
|
+
serverUrl
|
|
385
|
+
).toString()
|
|
386
|
+
reply.header('Location', createdUrl)
|
|
387
|
+
// Surface the new Collection Description ETag so a client can chain a
|
|
388
|
+
// conditional update (the `key-epochs` conditional-Collection-write feature).
|
|
389
|
+
reply.header('etag', formatEtag(written.version))
|
|
390
|
+
// Echo what was persisted, `createdBy` included, so the create response and
|
|
391
|
+
// a subsequent Get Collection agree. An id already in use was rejected as a
|
|
392
|
+
// 409 above, so this write created the Collection.
|
|
393
|
+
return reply
|
|
394
|
+
.status(201)
|
|
395
|
+
.send({ ...collectionDescription, ...(createdBy && { createdBy }) })
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* DELETE /space/:spaceId
|
|
400
|
+
* Request handler for "Delete Space" request
|
|
401
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
402
|
+
* request.zcap: {
|
|
403
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
404
|
+
* }
|
|
405
|
+
*
|
|
406
|
+
* @param request {import('fastify').FastifyRequest}
|
|
407
|
+
* @param reply {import('fastify').FastifyReply}
|
|
408
|
+
* @returns {Promise<FastifyReply>}
|
|
409
|
+
*/
|
|
410
|
+
static async delete(
|
|
411
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
412
|
+
reply: FastifyReply
|
|
413
|
+
): Promise<FastifyReply> {
|
|
414
|
+
const {
|
|
415
|
+
params: { spaceId }
|
|
416
|
+
} = request
|
|
417
|
+
const { storage } = request.server
|
|
418
|
+
const requestName = 'Delete Space'
|
|
419
|
+
|
|
420
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
421
|
+
assertValidIds({ spaceId }, { requestName })
|
|
422
|
+
|
|
423
|
+
// Verify (capability-only): deleting a Space requires a valid capability
|
|
424
|
+
// invocation; no access-control-policy fallback.
|
|
425
|
+
await fetchSpaceAndVerify({
|
|
426
|
+
request,
|
|
427
|
+
spaceId,
|
|
428
|
+
targetPath: spacePath({ spaceId }),
|
|
429
|
+
requestName
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// zCap checks out, continue
|
|
433
|
+
await storage.deleteSpace({ spaceId })
|
|
434
|
+
// Bust the cached description so the next read sees the Space as gone (404).
|
|
435
|
+
invalidateSpaceDescription({ storage, spaceId })
|
|
436
|
+
|
|
437
|
+
return reply.status(204).send()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* POST /space/:spaceId/export
|
|
442
|
+
* Request handler for "Export Space" request
|
|
443
|
+
*
|
|
444
|
+
* @param request {import('fastify').FastifyRequest}
|
|
445
|
+
* @param reply {import('fastify').FastifyReply}
|
|
446
|
+
* @returns {Promise<FastifyReply>}
|
|
447
|
+
*/
|
|
448
|
+
static async export(
|
|
449
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
450
|
+
reply: FastifyReply
|
|
451
|
+
): Promise<FastifyReply> {
|
|
452
|
+
const {
|
|
453
|
+
params: { spaceId }
|
|
454
|
+
} = request
|
|
455
|
+
const { storage } = request.server
|
|
456
|
+
const requestName = 'Export Space'
|
|
457
|
+
|
|
458
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
459
|
+
assertValidIds({ spaceId }, { requestName })
|
|
460
|
+
|
|
461
|
+
// Verify (capability-only): exporting a Space requires a valid capability
|
|
462
|
+
// invocation; no access-control-policy fallback.
|
|
463
|
+
await fetchSpaceAndVerify({
|
|
464
|
+
request,
|
|
465
|
+
spaceId,
|
|
466
|
+
targetPath: exportPath({ spaceId }),
|
|
467
|
+
requestName
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// zCap checks out, continue
|
|
471
|
+
const tarFile = await storage.exportSpace({ spaceId })
|
|
472
|
+
|
|
473
|
+
return reply.status(200).type('application/x-tar').send(tarFile)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* POST /space/:spaceId/import
|
|
478
|
+
* Request handler for "Import Space" request (merge from tarball)
|
|
479
|
+
*
|
|
480
|
+
* @param request {import('fastify').FastifyRequest}
|
|
481
|
+
* @param reply {import('fastify').FastifyReply}
|
|
482
|
+
* @returns {Promise<FastifyReply>}
|
|
483
|
+
*/
|
|
484
|
+
static async import(
|
|
485
|
+
request: FastifyRequest<{ Params: { spaceId: string }; Body: Readable }>,
|
|
486
|
+
reply: FastifyReply
|
|
487
|
+
): Promise<FastifyReply> {
|
|
488
|
+
const {
|
|
489
|
+
params: { spaceId }
|
|
490
|
+
} = request
|
|
491
|
+
const { storage } = request.server
|
|
492
|
+
const requestName = 'Import Space'
|
|
493
|
+
|
|
494
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
495
|
+
assertValidIds({ spaceId }, { requestName })
|
|
496
|
+
|
|
497
|
+
// Verify (capability-only): importing into a Space requires a valid
|
|
498
|
+
// capability invocation; no access-control-policy fallback.
|
|
499
|
+
await fetchSpaceAndVerify({
|
|
500
|
+
request,
|
|
501
|
+
spaceId,
|
|
502
|
+
targetPath: importPath({ spaceId }),
|
|
503
|
+
requestName
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const summary = await storage.importSpace({
|
|
508
|
+
spaceId,
|
|
509
|
+
tarStream: request.body
|
|
510
|
+
})
|
|
511
|
+
return reply.status(200).send(summary)
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// Archive-validation failures already surface as typed ProblemErrors
|
|
514
|
+
// (e.g. InvalidImportError from the manifest checks, or an invalid-id
|
|
515
|
+
// error from a malformed archive id) -- let those through unchanged,
|
|
516
|
+
// preserving their status code and message. Anything else is an
|
|
517
|
+
// unexpected failure decoding the upload: wrap it as a generic
|
|
518
|
+
// invalid-import 400, keeping the original as the `cause`.
|
|
519
|
+
if (err instanceof ProblemError) {
|
|
520
|
+
throw err
|
|
521
|
+
}
|
|
522
|
+
throw new InvalidImportError({ cause: err as Error })
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* GET /space/:spaceId/collections/
|
|
528
|
+
* Request handler for "List Collections" request
|
|
529
|
+
*
|
|
530
|
+
* @param request {import('fastify').FastifyRequest}
|
|
531
|
+
* @param reply {import('fastify').FastifyReply}
|
|
532
|
+
* @returns {Promise<FastifyReply>}
|
|
533
|
+
*/
|
|
534
|
+
static async listCollections(
|
|
535
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
536
|
+
reply: FastifyReply
|
|
537
|
+
): Promise<FastifyReply> {
|
|
538
|
+
const {
|
|
539
|
+
params: { spaceId }
|
|
540
|
+
} = request
|
|
541
|
+
const { storage } = request.server
|
|
542
|
+
const requestName = 'List Collections'
|
|
543
|
+
|
|
544
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
545
|
+
assertValidIds({ spaceId }, { requestName })
|
|
546
|
+
|
|
547
|
+
// Authorize (capability-or-policy): capability invocation first, then the
|
|
548
|
+
// Space's access-control policy as a fallback (a public-readable Space).
|
|
549
|
+
await fetchSpaceAndAuthorize({
|
|
550
|
+
request,
|
|
551
|
+
spaceId,
|
|
552
|
+
targetPath: collectionsPath({ spaceId }),
|
|
553
|
+
requestName
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const collections = await storage.listCollections({ spaceId })
|
|
557
|
+
return reply.status(200).send({
|
|
558
|
+
url: collectionsPath({ spaceId }),
|
|
559
|
+
totalItems: collections.length,
|
|
560
|
+
items: collections
|
|
561
|
+
} satisfies CollectionsList)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* GET /space/:spaceId/backends
|
|
566
|
+
* Request handler for the "Space Backends Available" request: the list of
|
|
567
|
+
* storage backends registered for the Space. This reference server ships a
|
|
568
|
+
* single server-configured backend (registered as `default`), so the list has
|
|
569
|
+
* one entry, derived from the active backend's own `describe()`.
|
|
570
|
+
*
|
|
571
|
+
* Authorization is capability-or-policy, the same as List Collections: the
|
|
572
|
+
* backends list is no more sensitive than the Space description, so a
|
|
573
|
+
* public-readable Space may also list its backends.
|
|
574
|
+
*
|
|
575
|
+
* @param request {import('fastify').FastifyRequest}
|
|
576
|
+
* @param reply {import('fastify').FastifyReply}
|
|
577
|
+
* @returns {Promise<FastifyReply>}
|
|
578
|
+
*/
|
|
579
|
+
static async listBackends(
|
|
580
|
+
request: FastifyRequest<{ Params: { spaceId: string } }>,
|
|
581
|
+
reply: FastifyReply
|
|
582
|
+
): Promise<FastifyReply> {
|
|
583
|
+
const {
|
|
584
|
+
params: { spaceId }
|
|
585
|
+
} = request
|
|
586
|
+
const { storage } = request.server
|
|
587
|
+
const requestName = 'List Backends'
|
|
588
|
+
|
|
589
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
590
|
+
assertValidIds({ spaceId }, { requestName })
|
|
591
|
+
|
|
592
|
+
// Authorize (capability-or-policy): capability invocation first, then the
|
|
593
|
+
// Space's access-control policy as a fallback (a public-readable Space).
|
|
594
|
+
await fetchSpaceAndAuthorize({
|
|
595
|
+
request,
|
|
596
|
+
spaceId,
|
|
597
|
+
targetPath: backendsPath({ spaceId }),
|
|
598
|
+
requestName
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
return reply
|
|
602
|
+
.status(200)
|
|
603
|
+
.type('application/json')
|
|
604
|
+
.send(await listRegisteredBackends({ storage, spaceId }))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* GET /space/:spaceId/quotas
|
|
609
|
+
* Request handler for the "Quotas" request: the Space's storage report,
|
|
610
|
+
* grouped by backend (spec "Quotas"). This reference server ships a single
|
|
611
|
+
* server-configured backend, so the `backends` array has one entry, measured
|
|
612
|
+
* from the active backend's `reportUsage()`.
|
|
613
|
+
*
|
|
614
|
+
* The per-Collection `usageByCollection` breakdown is opt-in via the spec's
|
|
615
|
+
* `?include=collections` query parameter (omitted otherwise, to keep the
|
|
616
|
+
* hot-path payload lean). Reading that query string on a capability-signed
|
|
617
|
+
* request requires `allowTargetQuery` on the authorization call -- the spec's
|
|
618
|
+
* "Quotas" / "Pagination parameters and authorization" rule that a query
|
|
619
|
+
* parameter selecting a representation does not change the target a capability
|
|
620
|
+
* must match (see `verifyZcap`).
|
|
621
|
+
*
|
|
622
|
+
* Authorization is capability-or-policy, the same as List Collections and the
|
|
623
|
+
* backends list: a caller not authorized to read the report receives a 404
|
|
624
|
+
* (the spec's maximum-privacy invariant), and a public-readable Space may read
|
|
625
|
+
* its quota report. Authorization runs before the query is consulted.
|
|
626
|
+
*
|
|
627
|
+
* @param request {import('fastify').FastifyRequest}
|
|
628
|
+
* @param reply {import('fastify').FastifyReply}
|
|
629
|
+
* @returns {Promise<FastifyReply>}
|
|
630
|
+
*/
|
|
631
|
+
static async quotas(
|
|
632
|
+
request: FastifyRequest<{
|
|
633
|
+
Params: { spaceId: string }
|
|
634
|
+
Querystring: { include?: string | string[] }
|
|
635
|
+
}>,
|
|
636
|
+
reply: FastifyReply
|
|
637
|
+
): Promise<FastifyReply> {
|
|
638
|
+
const {
|
|
639
|
+
params: { spaceId },
|
|
640
|
+
query: { include }
|
|
641
|
+
} = request
|
|
642
|
+
const { storage } = request.server
|
|
643
|
+
const requestName = 'Get Quotas'
|
|
644
|
+
|
|
645
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
646
|
+
assertValidIds({ spaceId }, { requestName })
|
|
647
|
+
|
|
648
|
+
// Authorize (capability-or-policy): capability invocation first, then the
|
|
649
|
+
// Space's access-control policy as a fallback (a public-readable Space).
|
|
650
|
+
// `allowTargetQuery` lets the signed-request path tolerate the
|
|
651
|
+
// `?include=collections` query parameter without it changing the capability
|
|
652
|
+
// target.
|
|
653
|
+
await fetchSpaceAndAuthorize({
|
|
654
|
+
request,
|
|
655
|
+
spaceId,
|
|
656
|
+
targetPath: quotasPath({ spaceId }),
|
|
657
|
+
requestName,
|
|
658
|
+
allowTargetQuery: true
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
// The per-Collection breakdown is opt-in via `?include=collections` (spec
|
|
662
|
+
// "Quotas"); `include` is a comma-separated list of optional sections. A
|
|
663
|
+
// repeated `?include=` makes Fastify's default parser yield a string array,
|
|
664
|
+
// so normalize to an array first -- calling `.split` on the array would 500
|
|
665
|
+
// (unauthenticated-reachable on a public-readable Space).
|
|
666
|
+
const includeValues = Array.isArray(include)
|
|
667
|
+
? include
|
|
668
|
+
: include !== undefined
|
|
669
|
+
? [include]
|
|
670
|
+
: []
|
|
671
|
+
const includeCollections = includeValues
|
|
672
|
+
.flatMap(value => value.split(','))
|
|
673
|
+
.map(section => section.trim())
|
|
674
|
+
.includes('collections')
|
|
675
|
+
|
|
676
|
+
const usage = await storage.reportUsage({ spaceId, includeCollections })
|
|
677
|
+
|
|
678
|
+
return reply
|
|
679
|
+
.status(200)
|
|
680
|
+
.type('application/json')
|
|
681
|
+
.send({
|
|
682
|
+
respondedAt: new Date().toISOString(),
|
|
683
|
+
backends: [usage]
|
|
684
|
+
} satisfies SpaceQuotaReport)
|
|
685
|
+
}
|
|
686
|
+
}
|