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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handler for SpacesRepository operations:
|
|
3
|
+
* - POST /spaces/ (Create Space)
|
|
4
|
+
* - GET /spaces/ (List Spaces).
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
8
|
+
import { isRootInvocation, verifyZcap } from '../zcap.js'
|
|
9
|
+
import { invalidateSpaceDescription } from './spaceContext.js'
|
|
10
|
+
import { verifyBodyControllerConsent } from './controllerConsent.js'
|
|
11
|
+
import { invokerDid } from '../auth-header-hooks.js'
|
|
12
|
+
import { assertValidId } from '../lib/validateId.js'
|
|
13
|
+
import { spacePath, spacesPath } from '../lib/paths.js'
|
|
14
|
+
import { assertValidController } from '../lib/validateDid.js'
|
|
15
|
+
import {
|
|
16
|
+
SpaceControllerMismatchError,
|
|
17
|
+
InvalidRequestBodyError,
|
|
18
|
+
IdConflictError
|
|
19
|
+
} from '../errors.js'
|
|
20
|
+
import type { IDID, SpaceSummary, SpaceListing } from '../types.js'
|
|
21
|
+
|
|
22
|
+
export class SpacesRepositoryRequest {
|
|
23
|
+
/**
|
|
24
|
+
* GET /spaces/
|
|
25
|
+
* Request handler for "List Spaces" (spec "List Spaces Operation"): returns
|
|
26
|
+
* `{ url, totalItems, items }` with only the Spaces the caller is authorized
|
|
27
|
+
* to see. An anonymous or unauthorized request is NOT an error -- it gets the
|
|
28
|
+
* empty-items 200, the spec's explicit exception to 404 masking -- so nothing
|
|
29
|
+
* is revealed about which Spaces exist.
|
|
30
|
+
*
|
|
31
|
+
* Authorization is per Space controller: the root capability for `/spaces/`
|
|
32
|
+
* is synthesized with the candidate Space's controller (see `verifyZcap`), so
|
|
33
|
+
* one verification decides visibility for every Space sharing that
|
|
34
|
+
* controller. A bare-root invocation can only verify where the signer *is*
|
|
35
|
+
* the controller, so those candidates are filtered before any signature
|
|
36
|
+
* work; a delegated invocation reveals the Spaces of whichever controller
|
|
37
|
+
* roots its capability chain.
|
|
38
|
+
*
|
|
39
|
+
* @param request {import('fastify').FastifyRequest}
|
|
40
|
+
* @param reply {import('fastify').FastifyReply}
|
|
41
|
+
* @returns {Promise<FastifyReply>}
|
|
42
|
+
*/
|
|
43
|
+
static async get(
|
|
44
|
+
request: FastifyRequest,
|
|
45
|
+
reply: FastifyReply
|
|
46
|
+
): Promise<FastifyReply> {
|
|
47
|
+
const { url, method, headers } = request
|
|
48
|
+
const { serverUrl, storage } = request.server
|
|
49
|
+
|
|
50
|
+
const items: SpaceSummary[] = []
|
|
51
|
+
const listing: SpaceListing = { url: spacesPath(), totalItems: 0, items }
|
|
52
|
+
|
|
53
|
+
// No (complete) authorization presented: authorized to see no Spaces,
|
|
54
|
+
// which is the empty 200, not an error.
|
|
55
|
+
if (!request.zcap?.invocation) {
|
|
56
|
+
return reply.send(listing)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { keyId, invocation } = request.zcap
|
|
60
|
+
const [zcapSigningDid] = keyId.split('#')
|
|
61
|
+
const rootInvocation = isRootInvocation({ invocation })
|
|
62
|
+
const allowedTarget = new URL(spacesPath(), serverUrl).toString()
|
|
63
|
+
|
|
64
|
+
// Visibility is identical across Spaces sharing a controller (the
|
|
65
|
+
// verification depends only on the controller), so verify once per
|
|
66
|
+
// distinct controller. A failed verification just excludes that
|
|
67
|
+
// controller's Spaces -- never an error response.
|
|
68
|
+
const verifiedByController = new Map<IDID, boolean>()
|
|
69
|
+
for (const space of await storage.listSpaces()) {
|
|
70
|
+
const { controller } = space
|
|
71
|
+
if (rootInvocation && controller !== zcapSigningDid) {
|
|
72
|
+
continue // a bare-root invocation cannot verify for another controller
|
|
73
|
+
}
|
|
74
|
+
let authorized = verifiedByController.get(controller)
|
|
75
|
+
if (authorized === undefined) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await verifyZcap({
|
|
78
|
+
url,
|
|
79
|
+
allowedTarget,
|
|
80
|
+
allowedAction: 'GET',
|
|
81
|
+
method,
|
|
82
|
+
headers,
|
|
83
|
+
serverUrl,
|
|
84
|
+
spaceController: controller
|
|
85
|
+
})
|
|
86
|
+
authorized = result.verified === true
|
|
87
|
+
} catch (err) {
|
|
88
|
+
request.log.debug(
|
|
89
|
+
{ err },
|
|
90
|
+
'List Spaces: invocation did not verify for a candidate controller'
|
|
91
|
+
)
|
|
92
|
+
authorized = false
|
|
93
|
+
}
|
|
94
|
+
verifiedByController.set(controller, authorized)
|
|
95
|
+
}
|
|
96
|
+
if (authorized) {
|
|
97
|
+
const item: SpaceSummary = {
|
|
98
|
+
id: space.id,
|
|
99
|
+
url: spacePath({ spaceId: space.id })
|
|
100
|
+
}
|
|
101
|
+
if (space.name !== undefined) {
|
|
102
|
+
item.name = space.name
|
|
103
|
+
}
|
|
104
|
+
items.push(item)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
listing.totalItems = items.length
|
|
109
|
+
return reply.send(listing)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /spaces/
|
|
114
|
+
* Request handler for "Create Space" request
|
|
115
|
+
* Before this, `parseAuthHeaders()` hook executed, resulting in:
|
|
116
|
+
* request.zcap: {
|
|
117
|
+
* keyId, headers, signature, created, expires, invocation, digest
|
|
118
|
+
* }
|
|
119
|
+
*
|
|
120
|
+
* @param request {import('fastify').FastifyRequest}
|
|
121
|
+
* @param reply {import('fastify').FastifyReply}
|
|
122
|
+
* @returns {Promise<FastifyReply>}
|
|
123
|
+
*/
|
|
124
|
+
static async post(
|
|
125
|
+
request: FastifyRequest<{
|
|
126
|
+
Body: { id?: string; name?: string; controller: IDID }
|
|
127
|
+
}>,
|
|
128
|
+
reply: FastifyReply
|
|
129
|
+
): Promise<FastifyReply> {
|
|
130
|
+
const { body } = request
|
|
131
|
+
const { serverUrl, storage } = request.server
|
|
132
|
+
|
|
133
|
+
// The Space Description body must carry a controller DID. The `name`
|
|
134
|
+
// property is optional (see spec: Space Description object).
|
|
135
|
+
if (!body?.controller) {
|
|
136
|
+
throw new InvalidRequestBodyError({
|
|
137
|
+
requestName: 'Create Space',
|
|
138
|
+
detail: 'Space Description body requires a "controller" property.',
|
|
139
|
+
pointer: '#/controller'
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
// Reject a malformed / non-`did:key` controller before it is stored.
|
|
143
|
+
assertValidController(body.controller, { requestName: 'Create Space' })
|
|
144
|
+
// Reject a path-traversal / non-URL-safe client-supplied space id.
|
|
145
|
+
if (body.id !== undefined) {
|
|
146
|
+
assertValidId(body.id, { kind: 'space', requestName: 'Create Space' })
|
|
147
|
+
// POST must never replace an existing Space: the signature below is
|
|
148
|
+
// verified against the *body's* controller, so without this check any
|
|
149
|
+
// caller could overwrite a Space (controller included) by POSTing its
|
|
150
|
+
// id. Spec: `id-conflict` (409); create-or-replace by id is PUT's job.
|
|
151
|
+
if (await storage.getSpaceDescription({ spaceId: body.id })) {
|
|
152
|
+
throw new IdConflictError({ kind: 'Space' })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const spaceId = body.id || uuidv4()
|
|
157
|
+
const spaceDescription = { ...body, id: spaceId, type: ['Space'] }
|
|
158
|
+
|
|
159
|
+
// The invocation must be *authorized by* the body's controller (spec:
|
|
160
|
+
// Create Space): signed directly by it, or via a delegation chain rooted
|
|
161
|
+
// in it (see `verifyBodyControllerConsent`). Skipped when the provisioning
|
|
162
|
+
// policy already vouched for the request (e.g. a valid onboarding token).
|
|
163
|
+
if (!request.provisioningAuthorized) {
|
|
164
|
+
await verifyBodyControllerConsent({
|
|
165
|
+
request,
|
|
166
|
+
controller: body.controller,
|
|
167
|
+
allowedTarget: new URL(spacesPath(), serverUrl).toString(),
|
|
168
|
+
allowedAction: 'POST',
|
|
169
|
+
MismatchError: SpaceControllerMismatchError,
|
|
170
|
+
requestName: 'Create Space'
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// zCap checks out, continue. A token-provisioned create carries no
|
|
175
|
+
// invocation, so it records no `createdBy`.
|
|
176
|
+
const createdBy = invokerDid(request)
|
|
177
|
+
await storage.writeSpace({ spaceId, spaceDescription, createdBy })
|
|
178
|
+
// Bust any cached (e.g. negatively cached) description for this id so the
|
|
179
|
+
// next read sees the freshly created Space.
|
|
180
|
+
invalidateSpaceDescription({ storage, spaceId })
|
|
181
|
+
|
|
182
|
+
const createdSpaceUrl = new URL(
|
|
183
|
+
spacesPath({ spaceId }),
|
|
184
|
+
serverUrl
|
|
185
|
+
).toString()
|
|
186
|
+
reply.header('Location', createdSpaceUrl)
|
|
187
|
+
// Echo what was persisted, `createdBy` included, so the create response and
|
|
188
|
+
// a subsequent Get Space agree. An id already in use was rejected as a 409
|
|
189
|
+
// above, so this write created the Space and its creator is this invoker.
|
|
190
|
+
return reply
|
|
191
|
+
.status(201)
|
|
192
|
+
.send({ ...spaceDescription, ...(createdBy && { createdBy }) })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler prelude: fetch a Collection Description or 404 (paralleling
|
|
3
|
+
* spaceContext.ts / keystoreContext.ts). Nearly every Collection- and
|
|
4
|
+
* Resource-level handler repeats the same shape after authorization -- load
|
|
5
|
+
* the Collection Description for context, throw `CollectionNotFoundError`
|
|
6
|
+
* when absent -- so it lives here.
|
|
7
|
+
*/
|
|
8
|
+
import { CollectionNotFoundError } from '../errors.js'
|
|
9
|
+
import type { CollectionDescription, StorageBackend } from '../types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetches a Collection Description or throws CollectionNotFoundError (404)
|
|
13
|
+
* when absent.
|
|
14
|
+
* @param options {object}
|
|
15
|
+
* @param options.storage {StorageBackend} the request's storage backend
|
|
16
|
+
* @param options.spaceId {string}
|
|
17
|
+
* @param options.collectionId {string}
|
|
18
|
+
* @param options.requestName {string} human-readable request name, used in
|
|
19
|
+
* error titles
|
|
20
|
+
* @returns {Promise<CollectionDescription & { descriptionVersion?: number }>}
|
|
21
|
+
*/
|
|
22
|
+
export async function getCollectionOrThrow({
|
|
23
|
+
storage,
|
|
24
|
+
spaceId,
|
|
25
|
+
collectionId,
|
|
26
|
+
requestName
|
|
27
|
+
}: {
|
|
28
|
+
storage: StorageBackend
|
|
29
|
+
spaceId: string
|
|
30
|
+
collectionId: string
|
|
31
|
+
requestName: string
|
|
32
|
+
}): Promise<CollectionDescription & { descriptionVersion?: number }> {
|
|
33
|
+
const collectionDescription = await storage.getCollectionDescription({
|
|
34
|
+
spaceId,
|
|
35
|
+
collectionId
|
|
36
|
+
})
|
|
37
|
+
if (!collectionDescription) {
|
|
38
|
+
throw new CollectionNotFoundError({ requestName })
|
|
39
|
+
}
|
|
40
|
+
return collectionDescription
|
|
41
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler prelude for the create-by-consent bootstrap (paralleling
|
|
3
|
+
* spaceContext.ts / keystoreContext.ts): the operations that create a
|
|
4
|
+
* top-level resource owned by a DID named in the request *body* (Create
|
|
5
|
+
* Space, the create form of Update Space, Create Keystore) have no stored
|
|
6
|
+
* controller to verify against yet, so the invocation must be *authorized by*
|
|
7
|
+
* the body's controller -- signed directly by it, or via a delegation chain
|
|
8
|
+
* rooted in it. Verifying against the signer instead would let anyone install
|
|
9
|
+
* an unrelated, non-consenting DID as controller.
|
|
10
|
+
*/
|
|
11
|
+
import type { FastifyRequest } from 'fastify'
|
|
12
|
+
import { handleZcapVerify, isRootInvocation } from '../zcap.js'
|
|
13
|
+
import type { ProblemError } from '../errors.js'
|
|
14
|
+
import type { IDID } from '../types.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Verifies that the invocation is authorized by `controller` (the DID named
|
|
18
|
+
* in the request body). For the bare-root invocation form the signer *is* the
|
|
19
|
+
* invoker, so a mismatch is rejected up front, before any signature work. The
|
|
20
|
+
* delegated form instead carries a capability chain, judged by the
|
|
21
|
+
* verification (which synthesizes the root capability with `controller` as
|
|
22
|
+
* its controller): a delegated invocation that fails to verify is a chain not
|
|
23
|
+
* rooted in the body's controller, wrapped in `MismatchError` (spec
|
|
24
|
+
* `controller-mismatch`, 400). Root-form verification failures keep their
|
|
25
|
+
* generic errors -- the signer already matched the controller.
|
|
26
|
+
*
|
|
27
|
+
* @param options {object}
|
|
28
|
+
* @param options.request {FastifyRequest} supplies url, method, headers,
|
|
29
|
+
* logger, `request.zcap`, and `request.server` for serverUrl
|
|
30
|
+
* @param options.controller {IDID} the controller DID named in the request
|
|
31
|
+
* body, which must authorize the invocation
|
|
32
|
+
* @param options.allowedTarget {string} the capability's expected
|
|
33
|
+
* invocationTarget (full URL, including host and port)
|
|
34
|
+
* @param options.allowedAction {string} expected action, e.g. an HTTP verb
|
|
35
|
+
* @param options.MismatchError {new (options) => ProblemError} the
|
|
36
|
+
* operation's `controller-mismatch` error class (constructed with
|
|
37
|
+
* `{ zcapSigningDid, controller, cause? }`)
|
|
38
|
+
* @param [options.requestName] {string} human-readable request name, used
|
|
39
|
+
* in error titles
|
|
40
|
+
* @param [options.maxChainLength] {number} max delegation chain length,
|
|
41
|
+
* root included (see `verifyZcap`)
|
|
42
|
+
* @param [options.maxDelegationTtl] {number} max delegated-zcap TTL in
|
|
43
|
+
* milliseconds (see `verifyZcap`)
|
|
44
|
+
* @returns {Promise<void>}
|
|
45
|
+
*/
|
|
46
|
+
export async function verifyBodyControllerConsent({
|
|
47
|
+
request,
|
|
48
|
+
controller,
|
|
49
|
+
allowedTarget,
|
|
50
|
+
allowedAction,
|
|
51
|
+
MismatchError,
|
|
52
|
+
requestName,
|
|
53
|
+
maxChainLength,
|
|
54
|
+
maxDelegationTtl
|
|
55
|
+
}: {
|
|
56
|
+
request: FastifyRequest
|
|
57
|
+
controller: IDID
|
|
58
|
+
allowedTarget: string
|
|
59
|
+
allowedAction: string
|
|
60
|
+
MismatchError: new (options: {
|
|
61
|
+
zcapSigningDid: string
|
|
62
|
+
controller: string
|
|
63
|
+
cause?: Error
|
|
64
|
+
}) => ProblemError
|
|
65
|
+
requestName?: string
|
|
66
|
+
maxChainLength?: number
|
|
67
|
+
maxDelegationTtl?: number
|
|
68
|
+
}): Promise<void> {
|
|
69
|
+
const { url, method, headers } = request
|
|
70
|
+
const { serverUrl } = request.server
|
|
71
|
+
// The strict `requireAuthHeaders` hook guarantees auth headers were present
|
|
72
|
+
// and `parseAuthHeaders` set `request.zcap` before any calling handler.
|
|
73
|
+
const { keyId, invocation } = request.zcap!
|
|
74
|
+
const [zcapSigningDid] = keyId.split('#')
|
|
75
|
+
const rootInvocation = isRootInvocation({ invocation })
|
|
76
|
+
if (rootInvocation && zcapSigningDid !== controller) {
|
|
77
|
+
throw new MismatchError({ zcapSigningDid: zcapSigningDid!, controller })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await handleZcapVerify({
|
|
82
|
+
url,
|
|
83
|
+
allowedTarget,
|
|
84
|
+
allowedAction,
|
|
85
|
+
method,
|
|
86
|
+
headers,
|
|
87
|
+
serverUrl,
|
|
88
|
+
spaceController: controller,
|
|
89
|
+
requestName,
|
|
90
|
+
logger: request.log,
|
|
91
|
+
// Consent verifies a chain rooted in the BODY's controller for a
|
|
92
|
+
// resource that does not exist yet, so there is no keystore or Space
|
|
93
|
+
// scope a revocation could have been stored under.
|
|
94
|
+
revocation: 'no-revocation-scope',
|
|
95
|
+
maxChainLength,
|
|
96
|
+
maxDelegationTtl
|
|
97
|
+
})
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (!rootInvocation) {
|
|
100
|
+
throw new MismatchError({
|
|
101
|
+
zcapSigningDid: zcapSigningDid!,
|
|
102
|
+
controller,
|
|
103
|
+
cause: err as Error
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WebKMS keystore context helper (the `/kms` facet): the keystore
|
|
3
|
+
* analogue of `fetchSpaceAndVerify`
|
|
4
|
+
* (spaceContext.ts), used by the keystore-lifecycle, key-operation, and
|
|
5
|
+
* revocation request handlers.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyRequest } from 'fastify'
|
|
8
|
+
import { handleZcapVerify } from '../zcap.js'
|
|
9
|
+
import { isUrlSafeSegment } from '../lib/validateId.js'
|
|
10
|
+
import { kmsKeystoresPath } from '../lib/paths.js'
|
|
11
|
+
import {
|
|
12
|
+
KMS_MAX_CHAIN_LENGTH,
|
|
13
|
+
KMS_MAX_DELEGATION_TTL
|
|
14
|
+
} from '../config.default.js'
|
|
15
|
+
import { KeystoreNotFoundError } from '../errors.js'
|
|
16
|
+
import type { KeystoreConfig } from '../types.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads a keystore config by its URL param, masking an unknown (or
|
|
20
|
+
* non-URL-safe) id as the 404 `KeystoreNotFoundError` -- the WAS
|
|
21
|
+
* existence-masking convention. The verification-free half of
|
|
22
|
+
* `fetchKeystoreAndVerify`, for handlers that need the config before they can
|
|
23
|
+
* build their expected values (the revocation route).
|
|
24
|
+
*
|
|
25
|
+
* @param options {object}
|
|
26
|
+
* @param options.request {FastifyRequest} supplies `request.server.storage`
|
|
27
|
+
* @param options.keystoreId {string} the keystore's local id (URL param)
|
|
28
|
+
* @param options.requestName {string} request name used in error titles
|
|
29
|
+
* @returns {Promise<KeystoreConfig>} the stored config
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchKeystore({
|
|
32
|
+
request,
|
|
33
|
+
keystoreId,
|
|
34
|
+
requestName
|
|
35
|
+
}: {
|
|
36
|
+
request: FastifyRequest
|
|
37
|
+
keystoreId: string
|
|
38
|
+
requestName: string
|
|
39
|
+
}): Promise<KeystoreConfig> {
|
|
40
|
+
// A non-URL-safe id cannot name a stored keystore (ids are server-generated
|
|
41
|
+
// base58 values) and must not reach the filesystem layer: same 404 masking.
|
|
42
|
+
if (!isUrlSafeSegment(keystoreId)) {
|
|
43
|
+
throw new KeystoreNotFoundError({ requestName })
|
|
44
|
+
}
|
|
45
|
+
const config = await request.server.storage.getKeystore({ keystoreId })
|
|
46
|
+
if (!config) {
|
|
47
|
+
throw new KeystoreNotFoundError({ requestName })
|
|
48
|
+
}
|
|
49
|
+
return config
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Loads the keystore config (404 `KeystoreNotFoundError` when absent -- the
|
|
54
|
+
* WAS existence-masking convention) and verifies the capability invocation
|
|
55
|
+
* against the *stored* config's controller, with the keystore URL as the root
|
|
56
|
+
* invocation target. `allowedAction` is the webkms action (`read` / `write` /
|
|
57
|
+
* an operation name), not the HTTP verb. The verification carries the unified
|
|
58
|
+
* `/kms` delegation policy (`KMS_MAX_CHAIN_LENGTH`, `KMS_MAX_DELEGATION_TTL`;
|
|
59
|
+
* the zcap library itself already requires `expires` on every delegated
|
|
60
|
+
* capability) and the keystore's revocation-store chain inspector, so a
|
|
61
|
+
* revoked delegation fails on every keystore-rooted route.
|
|
62
|
+
*
|
|
63
|
+
* @param options {object}
|
|
64
|
+
* @param options.request {FastifyRequest} supplies url, method, headers,
|
|
65
|
+
* logger, and `request.server` for serverUrl + storage
|
|
66
|
+
* @param options.keystoreId {string} the keystore's local id (URL param)
|
|
67
|
+
* @param options.allowedAction {string} expected zcap action
|
|
68
|
+
* @param options.requestName {string} request name used in error titles
|
|
69
|
+
* @param [options.allowTargetAttenuation] {boolean} accept a request URL
|
|
70
|
+
* under the keystore URL (the key routes: every key operation roots in the
|
|
71
|
+
* keystore's capability, with the key URL as an attenuated target -- see
|
|
72
|
+
* `verifyZcap`)
|
|
73
|
+
* @param [options.allowTargetQuery] {boolean} tolerate query parameters on
|
|
74
|
+
* the request URL that are absent from the capability target (the List Keys
|
|
75
|
+
* route's `?limit` / `?cursor` pagination parameters select a page within an
|
|
76
|
+
* already-authorized target; the same treatment as List Keystores)
|
|
77
|
+
* @returns {Promise<{ config: KeystoreConfig, dereferencedChainLength: number }>}
|
|
78
|
+
* the stored config and the verified invocation's chain length, root
|
|
79
|
+
* included (a root invocation is 1) -- the input to the per-key
|
|
80
|
+
* `maxCapabilityChainLength` gate at operation time
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchKeystoreAndVerify({
|
|
83
|
+
request,
|
|
84
|
+
keystoreId,
|
|
85
|
+
allowedAction,
|
|
86
|
+
requestName,
|
|
87
|
+
allowTargetAttenuation = false,
|
|
88
|
+
allowTargetQuery = false
|
|
89
|
+
}: {
|
|
90
|
+
request: FastifyRequest
|
|
91
|
+
keystoreId: string
|
|
92
|
+
allowedAction: string
|
|
93
|
+
requestName: string
|
|
94
|
+
allowTargetAttenuation?: boolean
|
|
95
|
+
allowTargetQuery?: boolean
|
|
96
|
+
}): Promise<{ config: KeystoreConfig; dereferencedChainLength: number }> {
|
|
97
|
+
const { serverUrl, storage } = request.server
|
|
98
|
+
const config = await fetchKeystore({ request, keystoreId, requestName })
|
|
99
|
+
const { url, method, headers } = request
|
|
100
|
+
const allowedTarget = new URL(
|
|
101
|
+
kmsKeystoresPath({ keystoreId }),
|
|
102
|
+
serverUrl
|
|
103
|
+
).toString()
|
|
104
|
+
const zcapVerifyResult = await handleZcapVerify({
|
|
105
|
+
url,
|
|
106
|
+
allowedTarget,
|
|
107
|
+
allowedAction,
|
|
108
|
+
method,
|
|
109
|
+
headers,
|
|
110
|
+
serverUrl,
|
|
111
|
+
spaceController: config.controller,
|
|
112
|
+
requestName,
|
|
113
|
+
logger: request.log,
|
|
114
|
+
allowTargetAttenuation,
|
|
115
|
+
allowTargetQuery,
|
|
116
|
+
revocation: { storage, scope: { keystoreId } },
|
|
117
|
+
maxChainLength: KMS_MAX_CHAIN_LENGTH,
|
|
118
|
+
maxDelegationTtl: KMS_MAX_DELEGATION_TTL
|
|
119
|
+
})
|
|
120
|
+
return {
|
|
121
|
+
config,
|
|
122
|
+
dereferencedChainLength: zcapVerifyResult.dereferencedChain?.length ?? 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-layer adapter: resolves a Fastify request into a transport-neutral
|
|
3
|
+
* `ResourceInput` value object, so that storage backends never depend on
|
|
4
|
+
* Fastify. JSON bodies pass through as parsed values; raw blob bodies and
|
|
5
|
+
* multipart uploads are normalized to a readable byte stream.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyRequest } from 'fastify'
|
|
8
|
+
import { Readable } from 'node:stream'
|
|
9
|
+
import { isJson } from '../lib/isJson.js'
|
|
10
|
+
import {
|
|
11
|
+
MissingContentTypeError,
|
|
12
|
+
InvalidRequestBodyError,
|
|
13
|
+
PayloadTooLargeError
|
|
14
|
+
} from '../errors.js'
|
|
15
|
+
import type { ResourceInput, StorageBackend } from '../types.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param request {import('fastify').FastifyRequest}
|
|
19
|
+
* @param [dataBackend] {StorageBackend} the resolved data-plane backend the
|
|
20
|
+
* write targets (the Collection's selected backend); its `maxUploadBytes` and
|
|
21
|
+
* `describe().id` size and label the `payload-too-large` (413) for a multipart
|
|
22
|
+
* upload. Defaults to the server `storage` backend, read lazily only on the
|
|
23
|
+
* multipart path so non-multipart callers need not supply it.
|
|
24
|
+
* @returns {Promise<ResourceInput>}
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveResourceInput(
|
|
27
|
+
request: FastifyRequest,
|
|
28
|
+
dataBackend?: StorageBackend
|
|
29
|
+
): Promise<ResourceInput> {
|
|
30
|
+
const contentType = request.headers['content-type']
|
|
31
|
+
if (!contentType) {
|
|
32
|
+
throw new MissingContentTypeError({ requestName: 'Write Resource' })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isJson({ contentType })) {
|
|
36
|
+
return { kind: 'json', contentType, data: request.body }
|
|
37
|
+
}
|
|
38
|
+
if (contentType.startsWith('multipart')) {
|
|
39
|
+
// Spec ("Content Types and Representations"): a multipart write MUST carry
|
|
40
|
+
// exactly one file part -- reject zero file parts, or more than one, as
|
|
41
|
+
// `invalid-request-body` (400). Iterate every part rather than calling
|
|
42
|
+
// `request.file()` (which silently takes the first file part and ignores any
|
|
43
|
+
// extras). `toBuffer()` drains each file part's stream, which both lets the
|
|
44
|
+
// iterator advance to detect a second (disallowed) part and yields the exact
|
|
45
|
+
// byte length to pre-flight the upload cap. Buffering the part in memory is
|
|
46
|
+
// acceptable for this multipart convenience path (the HTML form workflow) --
|
|
47
|
+
// large binaries should use the streaming raw-body path. The buffer is
|
|
48
|
+
// bounded by the backend's `maxUploadBytes` (the multipart `fileSize` limit
|
|
49
|
+
// set in `plugin.ts`): `toBuffer()` throws `FST_REQ_FILE_TOO_LARGE` at the
|
|
50
|
+
// boundary, mapped here to `payload-too-large` (413).
|
|
51
|
+
const backend = dataBackend ?? request.server.storage
|
|
52
|
+
let file: { mimetype: string; bytes: Buffer } | undefined
|
|
53
|
+
for await (const part of request.parts()) {
|
|
54
|
+
if (part.type !== 'file') {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (file) {
|
|
58
|
+
throw new InvalidRequestBodyError({
|
|
59
|
+
requestName: 'Write Resource',
|
|
60
|
+
detail: 'multipart request must carry exactly one file part.'
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
file = { mimetype: part.mimetype, bytes: await part.toBuffer() }
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if ((err as { code?: string }).code === 'FST_REQ_FILE_TOO_LARGE') {
|
|
67
|
+
throw new PayloadTooLargeError({
|
|
68
|
+
maxUploadBytes: backend.maxUploadBytes!,
|
|
69
|
+
backendId: backend.describe().id
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
throw err
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!file) {
|
|
76
|
+
throw new InvalidRequestBodyError({
|
|
77
|
+
requestName: 'Write Resource',
|
|
78
|
+
detail: 'multipart request is missing a file part.'
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
kind: 'binary',
|
|
83
|
+
contentType: file.mimetype,
|
|
84
|
+
stream: Readable.from(file.bytes),
|
|
85
|
+
declaredBytes: file.bytes.length
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// A `text/plain` body arrives already parsed as a **string** (Fastify's
|
|
89
|
+
// built-in text parser, which the `*` catch-all does not shadow); other
|
|
90
|
+
// `text/*` and binary media types arrive as a raw stream. Wrap a string (or
|
|
91
|
+
// Buffer) as a byte stream so it is written verbatim -- piping a string into
|
|
92
|
+
// the backend's `stream.pipeline` would iterate it by UTF-16 code unit (one
|
|
93
|
+
// write per character, splitting astral-plane characters into lone
|
|
94
|
+
// surrogates), corrupting the stored bytes.
|
|
95
|
+
const body = request.body
|
|
96
|
+
if (typeof body === 'string' || Buffer.isBuffer(body)) {
|
|
97
|
+
const bytes = Buffer.from(body as string | Buffer)
|
|
98
|
+
return {
|
|
99
|
+
kind: 'binary',
|
|
100
|
+
contentType,
|
|
101
|
+
stream: Readable.from(bytes),
|
|
102
|
+
declaredBytes: bytes.length
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// A raw (non-multipart) blob body carries its size in `Content-Length` when
|
|
106
|
+
// present; expose it as `declaredBytes` so the backend can pre-flight the
|
|
107
|
+
// quota check before streaming. Ignore an absent or malformed value.
|
|
108
|
+
const contentLength = Number(request.headers['content-length'])
|
|
109
|
+
return {
|
|
110
|
+
kind: 'binary',
|
|
111
|
+
contentType,
|
|
112
|
+
stream: body as Readable,
|
|
113
|
+
declaredBytes:
|
|
114
|
+
Number.isInteger(contentLength) && contentLength >= 0
|
|
115
|
+
? contentLength
|
|
116
|
+
: undefined
|
|
117
|
+
}
|
|
118
|
+
}
|