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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handlers for Resource operations: create-by-id, get, and delete a
|
|
3
|
+
* Resource (JSON object or binary blob).
|
|
4
|
+
*/
|
|
5
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
6
|
+
export declare class ResourceRequest {
|
|
7
|
+
/**
|
|
8
|
+
* PUT /space/:spaceId/:collectionId/:resourceId
|
|
9
|
+
* Request handler for "Create (or Update) Resource by Id" request
|
|
10
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
11
|
+
* request.zcap: {
|
|
12
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* @param request {import('fastify').FastifyRequest}
|
|
16
|
+
* @param reply {import('fastify').FastifyReply}
|
|
17
|
+
* @returns {Promise<FastifyReply>}
|
|
18
|
+
*/
|
|
19
|
+
static put(request: FastifyRequest<{
|
|
20
|
+
Params: {
|
|
21
|
+
spaceId: string;
|
|
22
|
+
collectionId: string;
|
|
23
|
+
resourceId: string;
|
|
24
|
+
};
|
|
25
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
26
|
+
/**
|
|
27
|
+
* GET /space/:spaceId/:collectionId/:resourceId
|
|
28
|
+
* Request handler for "Get Resource" request
|
|
29
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
30
|
+
* request.zcap: {
|
|
31
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* @param request {import('fastify').FastifyRequest}
|
|
35
|
+
* @param reply {import('fastify').FastifyReply}
|
|
36
|
+
* @returns {Promise<FastifyReply>}
|
|
37
|
+
*/
|
|
38
|
+
static get(request: FastifyRequest<{
|
|
39
|
+
Params: {
|
|
40
|
+
spaceId: string;
|
|
41
|
+
collectionId: string;
|
|
42
|
+
resourceId: string;
|
|
43
|
+
};
|
|
44
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
45
|
+
/**
|
|
46
|
+
* HEAD /space/:spaceId/:collectionId/:resourceId
|
|
47
|
+
* Request handler for "Head Resource" request: the same authorization as Get
|
|
48
|
+
* Resource but with no response body. Per spec "Content Types and
|
|
49
|
+
* Representations", the response `Content-Type` and `Content-Length`
|
|
50
|
+
* correspond to the `contentType` and `size` of the Resource's Metadata object
|
|
51
|
+
* (the bytes a GET would return). HEAD is a safe method, authorized as a read
|
|
52
|
+
* (capability-or-policy), the same as GET; it reads only the Metadata so it
|
|
53
|
+
* never opens the resource byte stream.
|
|
54
|
+
*
|
|
55
|
+
* Registered explicitly (ahead of the GET route) rather than relying on
|
|
56
|
+
* Fastify's auto-exposed HEAD, which would share the GET handler and stream
|
|
57
|
+
* the whole body -- yielding no `Content-Length` for a streamed representation.
|
|
58
|
+
*
|
|
59
|
+
* @param request {import('fastify').FastifyRequest}
|
|
60
|
+
* @param reply {import('fastify').FastifyReply}
|
|
61
|
+
* @returns {Promise<FastifyReply>}
|
|
62
|
+
*/
|
|
63
|
+
static head(request: FastifyRequest<{
|
|
64
|
+
Params: {
|
|
65
|
+
spaceId: string;
|
|
66
|
+
collectionId: string;
|
|
67
|
+
resourceId: string;
|
|
68
|
+
};
|
|
69
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
70
|
+
/**
|
|
71
|
+
* GET /space/:spaceId/:collectionId/:resourceId/meta
|
|
72
|
+
* Request handler for "Read Resource Metadata" request. Returns the REQUIRED
|
|
73
|
+
* server-managed fields (`contentType`, `size`), the OPTIONAL `createdAt` /
|
|
74
|
+
* `updatedAt` timestamps, and the user-writable `custom` object (omitted when
|
|
75
|
+
* empty). Authorization is capability-or-policy, the same as Get Resource:
|
|
76
|
+
* metadata reveals nothing beyond what a GET of the resource itself exposes via
|
|
77
|
+
* Content-Type / Content-Length, so a `PublicCanRead` policy also grants
|
|
78
|
+
* metadata reads.
|
|
79
|
+
*
|
|
80
|
+
* @param request {import('fastify').FastifyRequest}
|
|
81
|
+
* @param reply {import('fastify').FastifyReply}
|
|
82
|
+
* @returns {Promise<FastifyReply>}
|
|
83
|
+
*/
|
|
84
|
+
static getMeta(request: FastifyRequest<{
|
|
85
|
+
Params: {
|
|
86
|
+
spaceId: string;
|
|
87
|
+
collectionId: string;
|
|
88
|
+
resourceId: string;
|
|
89
|
+
};
|
|
90
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
91
|
+
/**
|
|
92
|
+
* PUT /space/:spaceId/:collectionId/:resourceId/meta
|
|
93
|
+
* Request handler for "Update Resource Metadata" request. A full replacement
|
|
94
|
+
* of the Metadata object's user-writable `custom` object (any property omitted
|
|
95
|
+
* is cleared; a body with no `custom` clears them all). Server-managed
|
|
96
|
+
* properties are untouched, and any top-level property other than `custom` in
|
|
97
|
+
* the body is ignored (so a client may GET-modify-PUT the whole object). Does
|
|
98
|
+
* NOT create: a `PUT` to the `/meta` of a nonexistent Resource is a 404.
|
|
99
|
+
* Authorization is capability-only (the `PUT` action), the same as Put
|
|
100
|
+
* Resource. Returns 204.
|
|
101
|
+
*
|
|
102
|
+
* @param request {import('fastify').FastifyRequest}
|
|
103
|
+
* @param reply {import('fastify').FastifyReply}
|
|
104
|
+
* @returns {Promise<FastifyReply>}
|
|
105
|
+
*/
|
|
106
|
+
static putMeta(request: FastifyRequest<{
|
|
107
|
+
Params: {
|
|
108
|
+
spaceId: string;
|
|
109
|
+
collectionId: string;
|
|
110
|
+
resourceId: string;
|
|
111
|
+
};
|
|
112
|
+
Body: unknown;
|
|
113
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
114
|
+
/**
|
|
115
|
+
* DELETE /space/:spaceId/:collectionId/:resourceId
|
|
116
|
+
* Request handler for "Delete Resource" request
|
|
117
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
118
|
+
* request.zcap: {
|
|
119
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
120
|
+
* }
|
|
121
|
+
*
|
|
122
|
+
* @param request {import('fastify').FastifyRequest}
|
|
123
|
+
* @param reply {import('fastify').FastifyReply}
|
|
124
|
+
* @returns {Promise<FastifyReply>}
|
|
125
|
+
*/
|
|
126
|
+
static delete(request: FastifyRequest<{
|
|
127
|
+
Params: {
|
|
128
|
+
spaceId: string;
|
|
129
|
+
collectionId: string;
|
|
130
|
+
resourceId: string;
|
|
131
|
+
};
|
|
132
|
+
}>, reply: FastifyReply): Promise<FastifyReply>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=ResourceRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceRequest.d.ts","sourceRoot":"","sources":["../../src/requests/ResourceRequest.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA6F3D,qBAAa,eAAe;IAC1B;;;;;;;;;;;OAWG;WACU,GAAG,CACd,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KACtE,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;IA+ExB;;;;;;;;;;;OAWG;WACU,GAAG,CACd,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KACtE,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;IAgExB;;;;;;;;;;;;;;;;;OAiBG;WACU,IAAI,CACf,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KACtE,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;IAmExB;;;;;;;;;;;;;OAaG;WACU,OAAO,CAClB,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KACtE,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;IAoExB;;;;;;;;;;;;;;OAcG;WACU,OAAO,CAClB,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;QACrE,IAAI,EAAE,OAAO,CAAA;KACd,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;IA2GxB;;;;;;;;;;;OAWG;WACU,MAAM,CACjB,OAAO,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KACtE,CAAC,EACF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC;CAmDzB"}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { fetchSpaceAndAuthorize, fetchSpaceAndVerify } from './spaceContext.js';
|
|
2
|
+
import { getCollectionOrThrow } from './collectionContext.js';
|
|
3
|
+
import { resolveResourceInput } from './resourceInput.js';
|
|
4
|
+
import { invokerDid } from '../auth-header-hooks.js';
|
|
5
|
+
import { resolveBackend } from '../lib/backendRegistry.js';
|
|
6
|
+
import { assertEncryptedWriteConforms, assertEncryptedMetaConforms } from '../lib/encryption.js';
|
|
7
|
+
import { assertValidIds } from '../lib/validateId.js';
|
|
8
|
+
import { resourcePath, metaPath } from '../lib/paths.js';
|
|
9
|
+
import { formatEtag, parseWritePreconditions } from '../lib/etag.js';
|
|
10
|
+
import { parseKeyEpochHeader, parseMetaEpoch } from '../lib/keyEpoch.js';
|
|
11
|
+
import { InvalidRequestBodyError, ResourceNotFoundError, rethrowOrWrapStorageError } from '../errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* Validates and extracts the user-writable `custom` object from an Update
|
|
14
|
+
* Resource Metadata request body on a **plaintext** Collection. The body MUST be
|
|
15
|
+
* a JSON object; any top-level property other than `custom` is ignored (so a
|
|
16
|
+
* client may GET-modify-PUT the whole Metadata object). A missing `custom`
|
|
17
|
+
* clears all user-writable properties (returns `{}`). Throws
|
|
18
|
+
* `InvalidRequestBodyError` (400) when the body or `custom` shape is wrong.
|
|
19
|
+
*
|
|
20
|
+
* On an **encrypted** Collection this shape check does not apply -- `custom` is
|
|
21
|
+
* the opaque encryption envelope, validated structurally by
|
|
22
|
+
* {@link assertEncryptedMetaConforms} instead (a `422` on non-conformance).
|
|
23
|
+
* `putMeta` branches on the Collection's `encryption` marker after authorization.
|
|
24
|
+
* @param body {unknown} the parsed request body
|
|
25
|
+
* @returns {ResourceMetadataCustom}
|
|
26
|
+
*/
|
|
27
|
+
function parseCustomMetadata(body) {
|
|
28
|
+
const requestName = 'Update Resource Metadata';
|
|
29
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
30
|
+
throw new InvalidRequestBodyError({
|
|
31
|
+
requestName,
|
|
32
|
+
detail: 'Request body must be a JSON object.'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const { custom } = body;
|
|
36
|
+
if (custom === undefined) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
if (typeof custom !== 'object' || custom === null || Array.isArray(custom)) {
|
|
40
|
+
throw new InvalidRequestBodyError({
|
|
41
|
+
requestName,
|
|
42
|
+
detail: 'The `custom` property must be a JSON object.',
|
|
43
|
+
pointer: '/custom'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const { name, tags } = custom;
|
|
47
|
+
if (name !== undefined && typeof name !== 'string') {
|
|
48
|
+
throw new InvalidRequestBodyError({
|
|
49
|
+
requestName,
|
|
50
|
+
detail: 'The `custom.name` property must be a string.',
|
|
51
|
+
pointer: '/custom/name'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (tags !== undefined &&
|
|
55
|
+
(typeof tags !== 'object' || tags === null || Array.isArray(tags))) {
|
|
56
|
+
throw new InvalidRequestBodyError({
|
|
57
|
+
requestName,
|
|
58
|
+
detail: 'The `custom.tags` property must be a JSON object.',
|
|
59
|
+
pointer: '/custom/tags'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Tag values MUST be strings (spec: values SHOULD be strings; the wire type
|
|
63
|
+
// models them as `Record<string, string>`).
|
|
64
|
+
if (tags !== undefined &&
|
|
65
|
+
Object.values(tags).some(value => typeof value !== 'string')) {
|
|
66
|
+
throw new InvalidRequestBodyError({
|
|
67
|
+
requestName,
|
|
68
|
+
detail: 'Every `custom.tags` value must be a string.',
|
|
69
|
+
pointer: '/custom/tags'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...(name !== undefined && { name }),
|
|
74
|
+
...(tags !== undefined && { tags: tags })
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export class ResourceRequest {
|
|
78
|
+
/**
|
|
79
|
+
* PUT /space/:spaceId/:collectionId/:resourceId
|
|
80
|
+
* Request handler for "Create (or Update) Resource by Id" request
|
|
81
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
82
|
+
* request.zcap: {
|
|
83
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
84
|
+
* }
|
|
85
|
+
*
|
|
86
|
+
* @param request {import('fastify').FastifyRequest}
|
|
87
|
+
* @param reply {import('fastify').FastifyReply}
|
|
88
|
+
* @returns {Promise<FastifyReply>}
|
|
89
|
+
*/
|
|
90
|
+
static async put(request, reply) {
|
|
91
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
92
|
+
const { storage } = request.server;
|
|
93
|
+
const requestName = 'Put Resource';
|
|
94
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
95
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
96
|
+
// Verify (capability-only): creating/updating a Resource requires a valid
|
|
97
|
+
// capability invocation; no access-control-policy fallback.
|
|
98
|
+
await fetchSpaceAndVerify({
|
|
99
|
+
request,
|
|
100
|
+
spaceId,
|
|
101
|
+
targetPath: resourcePath({ spaceId, collectionId, resourceId }),
|
|
102
|
+
requestName
|
|
103
|
+
});
|
|
104
|
+
// zCap checks out, continue
|
|
105
|
+
// Fetch collection by id
|
|
106
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
107
|
+
storage,
|
|
108
|
+
spaceId,
|
|
109
|
+
collectionId,
|
|
110
|
+
requestName
|
|
111
|
+
});
|
|
112
|
+
// Fail-closed encryption enforcement: if the Collection declares a recognized
|
|
113
|
+
// `encryption` scheme, the content write MUST be a conforming envelope of it
|
|
114
|
+
// (right media type + envelope shape), else `encryption-scheme-mismatch`
|
|
115
|
+
// (422). Runs after auth (above) and the 404, before the body is resolved --
|
|
116
|
+
// so a wrong content type is rejected without consuming the upload, and the
|
|
117
|
+
// 422 is only observable by a caller already authorized to write here.
|
|
118
|
+
assertEncryptedWriteConforms({
|
|
119
|
+
collectionDescription,
|
|
120
|
+
contentType: request.headers['content-type'],
|
|
121
|
+
body: request.body
|
|
122
|
+
});
|
|
123
|
+
// Route resource bytes to the Collection's selected (data-plane) backend.
|
|
124
|
+
const dataBackend = await resolveBackend({
|
|
125
|
+
request,
|
|
126
|
+
spaceId,
|
|
127
|
+
collectionId,
|
|
128
|
+
collectionDescription
|
|
129
|
+
});
|
|
130
|
+
const input = await resolveResourceInput(request, dataBackend);
|
|
131
|
+
// A content write into an encrypted Collection MAY declare the key epoch it
|
|
132
|
+
// encrypted under via the `WAS-Key-Epoch` header (the `key-epochs` feature);
|
|
133
|
+
// the server stores it opaquely and clears it when absent (the new
|
|
134
|
+
// ciphertext's epoch is unknown -- a stale stamp is worse than none).
|
|
135
|
+
// Advisory, non-signature-covered metadata.
|
|
136
|
+
const { epoch } = parseKeyEpochHeader({
|
|
137
|
+
headers: request.headers,
|
|
138
|
+
requestName
|
|
139
|
+
});
|
|
140
|
+
// Surface any `If-Match` / `If-None-Match` write precondition to the storage
|
|
141
|
+
// layer, which evaluates it atomically with the write (returning 412
|
|
142
|
+
// `precondition-failed` on a mismatch -- rethrown unchanged below).
|
|
143
|
+
let written;
|
|
144
|
+
try {
|
|
145
|
+
written = await dataBackend.writeResource({
|
|
146
|
+
spaceId,
|
|
147
|
+
collectionId,
|
|
148
|
+
resourceId,
|
|
149
|
+
input,
|
|
150
|
+
createdBy: invokerDid(request),
|
|
151
|
+
epoch,
|
|
152
|
+
...parseWritePreconditions(request.headers)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
157
|
+
}
|
|
158
|
+
// Return the new ETag so a client can chain a subsequent conditional write.
|
|
159
|
+
return reply.status(204).header('etag', formatEtag(written.version)).send();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* GET /space/:spaceId/:collectionId/:resourceId
|
|
163
|
+
* Request handler for "Get Resource" request
|
|
164
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
165
|
+
* request.zcap: {
|
|
166
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
167
|
+
* }
|
|
168
|
+
*
|
|
169
|
+
* @param request {import('fastify').FastifyRequest}
|
|
170
|
+
* @param reply {import('fastify').FastifyReply}
|
|
171
|
+
* @returns {Promise<FastifyReply>}
|
|
172
|
+
*/
|
|
173
|
+
static async get(request, reply) {
|
|
174
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
175
|
+
const { storage } = request.server;
|
|
176
|
+
const requestName = 'Get Resource';
|
|
177
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
178
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
179
|
+
// Authorize (capability-or-policy): capability invocation first, then fall
|
|
180
|
+
// back to the effective access-control policy (e.g. a world-readable
|
|
181
|
+
// Resource). Throws on denial.
|
|
182
|
+
await fetchSpaceAndAuthorize({
|
|
183
|
+
request,
|
|
184
|
+
spaceId,
|
|
185
|
+
collectionId,
|
|
186
|
+
resourceId,
|
|
187
|
+
targetPath: resourcePath({ spaceId, collectionId, resourceId }),
|
|
188
|
+
requestName
|
|
189
|
+
});
|
|
190
|
+
// authorized, continue
|
|
191
|
+
// Fetch collection by id
|
|
192
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
193
|
+
storage,
|
|
194
|
+
spaceId,
|
|
195
|
+
collectionId,
|
|
196
|
+
requestName
|
|
197
|
+
});
|
|
198
|
+
// Read the bytes from the Collection's selected (data-plane) backend.
|
|
199
|
+
const dataBackend = await resolveBackend({
|
|
200
|
+
request,
|
|
201
|
+
spaceId,
|
|
202
|
+
collectionId,
|
|
203
|
+
collectionDescription
|
|
204
|
+
});
|
|
205
|
+
const contentType = request.headers['content-type'];
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = await dataBackend.getResource({
|
|
209
|
+
spaceId,
|
|
210
|
+
collectionId,
|
|
211
|
+
resourceId,
|
|
212
|
+
contentType
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// Rethrow a typed ProblemError unchanged -- `getResource`'s
|
|
217
|
+
// ResourceNotFoundError (404) for an absent resource, or a typed fault a
|
|
218
|
+
// data-plane backend raises -- and wrap anything unexpected as a 500.
|
|
219
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
220
|
+
}
|
|
221
|
+
const getReply = reply.status(200).type(result.storedResourceType);
|
|
222
|
+
// Surface the ETag validator (the conditional-writes feature) when the
|
|
223
|
+
// backend tracks a version for this Resource.
|
|
224
|
+
if (result.version !== undefined) {
|
|
225
|
+
getReply.header('etag', formatEtag(result.version));
|
|
226
|
+
}
|
|
227
|
+
return getReply.send(result.resourceStream);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* HEAD /space/:spaceId/:collectionId/:resourceId
|
|
231
|
+
* Request handler for "Head Resource" request: the same authorization as Get
|
|
232
|
+
* Resource but with no response body. Per spec "Content Types and
|
|
233
|
+
* Representations", the response `Content-Type` and `Content-Length`
|
|
234
|
+
* correspond to the `contentType` and `size` of the Resource's Metadata object
|
|
235
|
+
* (the bytes a GET would return). HEAD is a safe method, authorized as a read
|
|
236
|
+
* (capability-or-policy), the same as GET; it reads only the Metadata so it
|
|
237
|
+
* never opens the resource byte stream.
|
|
238
|
+
*
|
|
239
|
+
* Registered explicitly (ahead of the GET route) rather than relying on
|
|
240
|
+
* Fastify's auto-exposed HEAD, which would share the GET handler and stream
|
|
241
|
+
* the whole body -- yielding no `Content-Length` for a streamed representation.
|
|
242
|
+
*
|
|
243
|
+
* @param request {import('fastify').FastifyRequest}
|
|
244
|
+
* @param reply {import('fastify').FastifyReply}
|
|
245
|
+
* @returns {Promise<FastifyReply>}
|
|
246
|
+
*/
|
|
247
|
+
static async head(request, reply) {
|
|
248
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
249
|
+
const { storage } = request.server;
|
|
250
|
+
const requestName = 'Head Resource';
|
|
251
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
252
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
253
|
+
// Authorize (capability-or-policy): the same read decision as Get Resource,
|
|
254
|
+
// against the same target (a HEAD reveals nothing a GET would not).
|
|
255
|
+
await fetchSpaceAndAuthorize({
|
|
256
|
+
request,
|
|
257
|
+
spaceId,
|
|
258
|
+
collectionId,
|
|
259
|
+
resourceId,
|
|
260
|
+
targetPath: resourcePath({ spaceId, collectionId, resourceId }),
|
|
261
|
+
requestName
|
|
262
|
+
});
|
|
263
|
+
// authorized, continue
|
|
264
|
+
// Fetch collection by id
|
|
265
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
266
|
+
storage,
|
|
267
|
+
spaceId,
|
|
268
|
+
collectionId,
|
|
269
|
+
requestName
|
|
270
|
+
});
|
|
271
|
+
// Read Metadata from the Collection's selected (data-plane) backend.
|
|
272
|
+
const dataBackend = await resolveBackend({
|
|
273
|
+
request,
|
|
274
|
+
spaceId,
|
|
275
|
+
collectionId,
|
|
276
|
+
collectionDescription
|
|
277
|
+
});
|
|
278
|
+
let metadata;
|
|
279
|
+
try {
|
|
280
|
+
metadata = await dataBackend.getResourceMetadata({
|
|
281
|
+
spaceId,
|
|
282
|
+
collectionId,
|
|
283
|
+
resourceId
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
// Rethrow a typed ProblemError from the data-plane backend unchanged;
|
|
288
|
+
// wrap anything unexpected as a 500.
|
|
289
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
290
|
+
}
|
|
291
|
+
if (!metadata) {
|
|
292
|
+
throw new ResourceNotFoundError({ requestName });
|
|
293
|
+
}
|
|
294
|
+
// Set the payload headers a GET would send, but send no body. Fastify keeps
|
|
295
|
+
// a manually-set `Content-Length` on a bodyless send (it is not recomputed
|
|
296
|
+
// to 0).
|
|
297
|
+
const headReply = reply
|
|
298
|
+
.status(200)
|
|
299
|
+
.type(metadata.contentType)
|
|
300
|
+
.header('content-length', metadata.size);
|
|
301
|
+
if (metadata.version !== undefined) {
|
|
302
|
+
headReply.header('etag', formatEtag(metadata.version));
|
|
303
|
+
}
|
|
304
|
+
return headReply.send();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* GET /space/:spaceId/:collectionId/:resourceId/meta
|
|
308
|
+
* Request handler for "Read Resource Metadata" request. Returns the REQUIRED
|
|
309
|
+
* server-managed fields (`contentType`, `size`), the OPTIONAL `createdAt` /
|
|
310
|
+
* `updatedAt` timestamps, and the user-writable `custom` object (omitted when
|
|
311
|
+
* empty). Authorization is capability-or-policy, the same as Get Resource:
|
|
312
|
+
* metadata reveals nothing beyond what a GET of the resource itself exposes via
|
|
313
|
+
* Content-Type / Content-Length, so a `PublicCanRead` policy also grants
|
|
314
|
+
* metadata reads.
|
|
315
|
+
*
|
|
316
|
+
* @param request {import('fastify').FastifyRequest}
|
|
317
|
+
* @param reply {import('fastify').FastifyReply}
|
|
318
|
+
* @returns {Promise<FastifyReply>}
|
|
319
|
+
*/
|
|
320
|
+
static async getMeta(request, reply) {
|
|
321
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
322
|
+
const { storage } = request.server;
|
|
323
|
+
const requestName = 'Get Resource Metadata';
|
|
324
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
325
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
326
|
+
// Authorize (capability-or-policy): the capability's `invocationTarget` is
|
|
327
|
+
// the full `/meta` URL (matching the request URL), and the policy level
|
|
328
|
+
// resolves at the resource as for Get Resource.
|
|
329
|
+
await fetchSpaceAndAuthorize({
|
|
330
|
+
request,
|
|
331
|
+
spaceId,
|
|
332
|
+
collectionId,
|
|
333
|
+
resourceId,
|
|
334
|
+
targetPath: metaPath({ spaceId, collectionId, resourceId }),
|
|
335
|
+
requestName
|
|
336
|
+
});
|
|
337
|
+
// authorized, continue
|
|
338
|
+
// Fetch collection by id
|
|
339
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
340
|
+
storage,
|
|
341
|
+
spaceId,
|
|
342
|
+
collectionId,
|
|
343
|
+
requestName
|
|
344
|
+
});
|
|
345
|
+
// Read Metadata from the Collection's selected (data-plane) backend.
|
|
346
|
+
const dataBackend = await resolveBackend({
|
|
347
|
+
request,
|
|
348
|
+
spaceId,
|
|
349
|
+
collectionId,
|
|
350
|
+
collectionDescription
|
|
351
|
+
});
|
|
352
|
+
let metadata;
|
|
353
|
+
try {
|
|
354
|
+
metadata = await dataBackend.getResourceMetadata({
|
|
355
|
+
spaceId,
|
|
356
|
+
collectionId,
|
|
357
|
+
resourceId
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
// Rethrow a typed ProblemError from the data-plane backend unchanged;
|
|
362
|
+
// wrap anything unexpected as a 500.
|
|
363
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
364
|
+
}
|
|
365
|
+
if (!metadata) {
|
|
366
|
+
throw new ResourceNotFoundError({ requestName });
|
|
367
|
+
}
|
|
368
|
+
// `version` (content) and `metaVersion` (metadata) are out-of-band ETag
|
|
369
|
+
// validators, not part of the Resource Metadata wire body, so strip both
|
|
370
|
+
// before serializing. The `/meta` sub-resource carries its OWN ETag
|
|
371
|
+
// (`metaVersion`, V2) so a metadata-only edit does not disturb the content
|
|
372
|
+
// ETag; it is present only once metadata has been written.
|
|
373
|
+
const { version: _version, metaVersion, ...metadataBody } = metadata;
|
|
374
|
+
const metaReply = reply.status(200).type('application/json');
|
|
375
|
+
if (metaVersion !== undefined) {
|
|
376
|
+
metaReply.header('etag', formatEtag(metaVersion));
|
|
377
|
+
}
|
|
378
|
+
return metaReply.send(JSON.stringify(metadataBody));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* PUT /space/:spaceId/:collectionId/:resourceId/meta
|
|
382
|
+
* Request handler for "Update Resource Metadata" request. A full replacement
|
|
383
|
+
* of the Metadata object's user-writable `custom` object (any property omitted
|
|
384
|
+
* is cleared; a body with no `custom` clears them all). Server-managed
|
|
385
|
+
* properties are untouched, and any top-level property other than `custom` in
|
|
386
|
+
* the body is ignored (so a client may GET-modify-PUT the whole object). Does
|
|
387
|
+
* NOT create: a `PUT` to the `/meta` of a nonexistent Resource is a 404.
|
|
388
|
+
* Authorization is capability-only (the `PUT` action), the same as Put
|
|
389
|
+
* Resource. Returns 204.
|
|
390
|
+
*
|
|
391
|
+
* @param request {import('fastify').FastifyRequest}
|
|
392
|
+
* @param reply {import('fastify').FastifyReply}
|
|
393
|
+
* @returns {Promise<FastifyReply>}
|
|
394
|
+
*/
|
|
395
|
+
static async putMeta(request, reply) {
|
|
396
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
397
|
+
const { storage } = request.server;
|
|
398
|
+
const requestName = 'Update Resource Metadata';
|
|
399
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
400
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
401
|
+
// Pre-auth body shape (400): the body MUST be a JSON object. The deeper
|
|
402
|
+
// `custom` shape check is deferred until after authorization, where the
|
|
403
|
+
// Collection's `encryption` marker decides whether `custom` is a plaintext
|
|
404
|
+
// `{ name, tags }` (validated by `parseCustomMetadata`) or an opaque envelope
|
|
405
|
+
// (validated structurally by `assertEncryptedMetaConforms`) -- neither is
|
|
406
|
+
// knowable before reading the Collection Description, and gating the check on
|
|
407
|
+
// auth keeps a 422/400 observable only to a caller authorized to write here.
|
|
408
|
+
if (typeof request.body !== 'object' ||
|
|
409
|
+
request.body === null ||
|
|
410
|
+
Array.isArray(request.body)) {
|
|
411
|
+
throw new InvalidRequestBodyError({
|
|
412
|
+
requestName,
|
|
413
|
+
detail: 'Request body must be a JSON object.'
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// Verify (capability-only): writing metadata requires a valid capability
|
|
417
|
+
// invocation (the `PUT` action); no access-control-policy fallback.
|
|
418
|
+
await fetchSpaceAndVerify({
|
|
419
|
+
request,
|
|
420
|
+
spaceId,
|
|
421
|
+
targetPath: metaPath({ spaceId, collectionId, resourceId }),
|
|
422
|
+
requestName
|
|
423
|
+
});
|
|
424
|
+
// zCap checks out, continue
|
|
425
|
+
// Fetch collection by id
|
|
426
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
427
|
+
storage,
|
|
428
|
+
spaceId,
|
|
429
|
+
collectionId,
|
|
430
|
+
requestName
|
|
431
|
+
});
|
|
432
|
+
// Branch on the Collection's encryption marker. On an encrypted Collection
|
|
433
|
+
// the `custom` value MUST be a conforming envelope of the scheme (stored
|
|
434
|
+
// opaquely, `422` on a plaintext/malformed value); on a plaintext Collection
|
|
435
|
+
// it MUST be a well-formed `{ name, tags }` object (`400` otherwise).
|
|
436
|
+
let custom;
|
|
437
|
+
if (collectionDescription.encryption?.scheme !== undefined) {
|
|
438
|
+
const rawCustom = request.body.custom;
|
|
439
|
+
assertEncryptedMetaConforms({ collectionDescription, custom: rawCustom });
|
|
440
|
+
custom = rawCustom;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
custom = parseCustomMetadata(request.body);
|
|
444
|
+
}
|
|
445
|
+
// The key-epoch stamp (the `key-epochs` feature) MAY also be declared here
|
|
446
|
+
// as a top-level `epoch` member (a sibling of `custom`). Unlike `custom`
|
|
447
|
+
// (full-replace), an omitted `epoch` PRESERVES the stored value -- it
|
|
448
|
+
// describes the content write, not the metadata write. A present value must
|
|
449
|
+
// be a non-empty string (400).
|
|
450
|
+
const { epoch } = parseMetaEpoch({
|
|
451
|
+
body: request.body,
|
|
452
|
+
requestName
|
|
453
|
+
});
|
|
454
|
+
// Write Metadata to the Collection's selected (data-plane) backend. An
|
|
455
|
+
// `If-Match` / `If-None-Match` precondition (the `conditional-writes`
|
|
456
|
+
// feature) is evaluated on the `/meta` `metaVersion` atomically with the
|
|
457
|
+
// write; a mismatch surfaces as 412 `precondition-failed` (rethrown unchanged).
|
|
458
|
+
const dataBackend = await resolveBackend({
|
|
459
|
+
request,
|
|
460
|
+
spaceId,
|
|
461
|
+
collectionId,
|
|
462
|
+
collectionDescription
|
|
463
|
+
});
|
|
464
|
+
let written;
|
|
465
|
+
try {
|
|
466
|
+
written = await dataBackend.writeResourceMetadata({
|
|
467
|
+
spaceId,
|
|
468
|
+
collectionId,
|
|
469
|
+
resourceId,
|
|
470
|
+
custom,
|
|
471
|
+
epoch,
|
|
472
|
+
...parseWritePreconditions(request.headers)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
477
|
+
}
|
|
478
|
+
// A Metadata object cannot exist apart from its Resource: a PUT to the
|
|
479
|
+
// `/meta` of a nonexistent Resource is a 404 (this operation does not create).
|
|
480
|
+
if (!written) {
|
|
481
|
+
throw new ResourceNotFoundError({ requestName });
|
|
482
|
+
}
|
|
483
|
+
// Return the new `/meta` ETag (`metaVersion`) so a client can chain a
|
|
484
|
+
// subsequent conditional metadata write.
|
|
485
|
+
return reply
|
|
486
|
+
.status(204)
|
|
487
|
+
.header('etag', formatEtag(written.metaVersion))
|
|
488
|
+
.send();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* DELETE /space/:spaceId/:collectionId/:resourceId
|
|
492
|
+
* Request handler for "Delete Resource" request
|
|
493
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
494
|
+
* request.zcap: {
|
|
495
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
496
|
+
* }
|
|
497
|
+
*
|
|
498
|
+
* @param request {import('fastify').FastifyRequest}
|
|
499
|
+
* @param reply {import('fastify').FastifyReply}
|
|
500
|
+
* @returns {Promise<FastifyReply>}
|
|
501
|
+
*/
|
|
502
|
+
static async delete(request, reply) {
|
|
503
|
+
const { params: { spaceId, collectionId, resourceId } } = request;
|
|
504
|
+
const { storage } = request.server;
|
|
505
|
+
const requestName = 'Delete Resource';
|
|
506
|
+
// Reject path-traversal / non-URL-safe ids before any storage access.
|
|
507
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName });
|
|
508
|
+
// Verify (capability-only): deleting a Resource requires a valid capability
|
|
509
|
+
// invocation; no access-control-policy fallback.
|
|
510
|
+
await fetchSpaceAndVerify({
|
|
511
|
+
request,
|
|
512
|
+
spaceId,
|
|
513
|
+
targetPath: resourcePath({ spaceId, collectionId, resourceId }),
|
|
514
|
+
requestName
|
|
515
|
+
});
|
|
516
|
+
// Fetch collection by id
|
|
517
|
+
const collectionDescription = await getCollectionOrThrow({
|
|
518
|
+
storage,
|
|
519
|
+
spaceId,
|
|
520
|
+
collectionId,
|
|
521
|
+
requestName
|
|
522
|
+
});
|
|
523
|
+
// zCap checks out, continue. An `If-Match` precondition is evaluated by the
|
|
524
|
+
// storage layer atomically with the removal; a mismatch surfaces as 412
|
|
525
|
+
// `precondition-failed` (rethrown unchanged below).
|
|
526
|
+
const { ifMatch } = parseWritePreconditions(request.headers);
|
|
527
|
+
// Delete from the Collection's selected (data-plane) backend.
|
|
528
|
+
const dataBackend = await resolveBackend({
|
|
529
|
+
request,
|
|
530
|
+
spaceId,
|
|
531
|
+
collectionId,
|
|
532
|
+
collectionDescription
|
|
533
|
+
});
|
|
534
|
+
try {
|
|
535
|
+
await dataBackend.deleteResource({
|
|
536
|
+
spaceId,
|
|
537
|
+
collectionId,
|
|
538
|
+
resourceId,
|
|
539
|
+
ifMatch
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
rethrowOrWrapStorageError({ err, requestName });
|
|
544
|
+
}
|
|
545
|
+
return reply.status(204).send();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
//# sourceMappingURL=ResourceRequest.js.map
|