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
package/src/routes.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route layer: maps URL patterns to *Request handler methods. Each group first
|
|
3
|
+
* installs the `requireAuthHeadersOrPublicRead` then `parseAuthHeaders`
|
|
4
|
+
* onRequest hooks, and redirects slash/no-slash variants to the canonical form.
|
|
5
|
+
* (The WebKMS `/kms` group is the exception on both counts: it installs the
|
|
6
|
+
* strict `requireAuthHeaders` -- the webkms protocol has no public reads --
|
|
7
|
+
* and no slash redirects, since the protocol's URLs are exact.)
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
FastifyInstance,
|
|
11
|
+
FastifyPluginOptions,
|
|
12
|
+
FastifyReply,
|
|
13
|
+
FastifyRequest
|
|
14
|
+
} from 'fastify'
|
|
15
|
+
import { SpacesRepositoryRequest } from './requests/SpacesRepositoryRequest.js'
|
|
16
|
+
import { SpaceRequest } from './requests/SpaceRequest.js'
|
|
17
|
+
import { handleError } from './errors.js'
|
|
18
|
+
import { ResourceRequest } from './requests/ResourceRequest.js'
|
|
19
|
+
import { CollectionRequest } from './requests/CollectionRequest.js'
|
|
20
|
+
import { PolicyRequest } from './requests/PolicyRequest.js'
|
|
21
|
+
import { BackendRequest } from './requests/BackendRequest.js'
|
|
22
|
+
import { KeystoreRequest } from './requests/KeystoreRequest.js'
|
|
23
|
+
import { KeyRequest } from './requests/KeyRequest.js'
|
|
24
|
+
import { RevocationRequest } from './requests/RevocationRequest.js'
|
|
25
|
+
import {
|
|
26
|
+
parseAuthHeaders,
|
|
27
|
+
requireAuthHeaders,
|
|
28
|
+
requireAuthHeadersOrPublicRead
|
|
29
|
+
} from './auth-header-hooks.js'
|
|
30
|
+
import { captureRawBody, verifyBodyDigest } from './digest.js'
|
|
31
|
+
import { provisioningGate } from './provisioning.js'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Toggles the trailing slash on the request's actual path (preserving any query
|
|
35
|
+
* string), returning the canonical target for a slash/no-slash redirect. Built
|
|
36
|
+
* from `request.url` rather than the route template so the `Location` carries
|
|
37
|
+
* the concrete ids (`/space/abc123`), not the literal `/space/:spaceId` -- which
|
|
38
|
+
* a client cannot follow.
|
|
39
|
+
* @param url {string} the request URL (path plus optional query string)
|
|
40
|
+
* @param addSlash {boolean} append (true) or strip (false) the trailing slash
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function toggleTrailingSlash(url: string, addSlash: boolean): string {
|
|
44
|
+
const queryIndex = url.indexOf('?')
|
|
45
|
+
const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex)
|
|
46
|
+
const query = queryIndex === -1 ? '' : url.slice(queryIndex)
|
|
47
|
+
const canonical = addSlash
|
|
48
|
+
? pathPart.endsWith('/')
|
|
49
|
+
? pathPart
|
|
50
|
+
: `${pathPart}/`
|
|
51
|
+
: pathPart.endsWith('/')
|
|
52
|
+
? pathPart.slice(0, -1)
|
|
53
|
+
: pathPart
|
|
54
|
+
return `${canonical}${query}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Redirects to the trailing-slash canonical form of the request URL with a
|
|
59
|
+
* `308` (Permanent Redirect), which -- unlike the default `302` -- requires the
|
|
60
|
+
* client to replay the same method and body, so a redirected POST/PUT is not
|
|
61
|
+
* silently downgraded to GET.
|
|
62
|
+
* @param request {import('fastify').FastifyRequest}
|
|
63
|
+
* @param reply {import('fastify').FastifyReply}
|
|
64
|
+
* @returns {FastifyReply}
|
|
65
|
+
*/
|
|
66
|
+
function redirectAddSlash(
|
|
67
|
+
request: FastifyRequest,
|
|
68
|
+
reply: FastifyReply
|
|
69
|
+
): FastifyReply {
|
|
70
|
+
return reply.redirect(toggleTrailingSlash(request.url, true), 308)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Redirects to the no-trailing-slash canonical form of the request URL (see
|
|
75
|
+
* {@link redirectAddSlash} for the `308` rationale).
|
|
76
|
+
* @param request {import('fastify').FastifyRequest}
|
|
77
|
+
* @param reply {import('fastify').FastifyReply}
|
|
78
|
+
* @returns {FastifyReply}
|
|
79
|
+
*/
|
|
80
|
+
function redirectStripSlash(
|
|
81
|
+
request: FastifyRequest,
|
|
82
|
+
reply: FastifyReply
|
|
83
|
+
): FastifyReply {
|
|
84
|
+
return reply.redirect(toggleTrailingSlash(request.url, false), 308)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Registers SpacesRepository routes (POST/GET /spaces). Installs the
|
|
89
|
+
* `requireAuthHeadersOrPublicRead` then `parseAuthHeaders` onRequest hooks and
|
|
90
|
+
* the `handleError` error handler.
|
|
91
|
+
* @param app {import('fastify').FastifyInstance}
|
|
92
|
+
* @param options {object} Fastify plugin options
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
export async function initSpacesRepositoryRoutes(
|
|
96
|
+
app: FastifyInstance,
|
|
97
|
+
_options: FastifyPluginOptions
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
app.setErrorHandler(handleError)
|
|
100
|
+
|
|
101
|
+
// Gate provisioning (Create Space): the configured policy may grant/deny, or
|
|
102
|
+
// (the default) allow -- in which case the normal zcap path below runs.
|
|
103
|
+
app.addHook('onRequest', provisioningGate)
|
|
104
|
+
// Create Space (POST) requires auth-related headers (401 otherwise); a List
|
|
105
|
+
// Spaces read may proceed unauthenticated -- an anonymous list is the spec's
|
|
106
|
+
// empty-items 200, never an error (the exception to 404 masking).
|
|
107
|
+
app.addHook('onRequest', requireAuthHeadersOrPublicRead)
|
|
108
|
+
// Parse the relevant request headers, set the request.zcap parameter
|
|
109
|
+
app.addHook('onRequest', parseAuthHeaders)
|
|
110
|
+
// Capture raw body bytes (JSON/text) so the digest can be recomputed against
|
|
111
|
+
// exactly what the client signed (spec "Request Body Integrity").
|
|
112
|
+
app.addHook('preParsing', captureRawBody)
|
|
113
|
+
// Enforce the Digest header binding: require it covered by the signature and,
|
|
114
|
+
// when the raw body is available, recompute and compare it.
|
|
115
|
+
app.addHook('preValidation', verifyBodyDigest)
|
|
116
|
+
|
|
117
|
+
// Add a Space to a SpacesRepository (Create Space)
|
|
118
|
+
app.post('/spaces', redirectAddSlash)
|
|
119
|
+
app.post('/spaces/', SpacesRepositoryRequest.post)
|
|
120
|
+
|
|
121
|
+
// List Spaces
|
|
122
|
+
app.get('/spaces', redirectAddSlash)
|
|
123
|
+
app.get('/spaces/', SpacesRepositoryRequest.get)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Registers Space routes (get/update/delete a Space, add/list Collections,
|
|
128
|
+
* export/import). Installs the auth hooks and the `handleError` error handler.
|
|
129
|
+
* @param app {import('fastify').FastifyInstance}
|
|
130
|
+
* @param options {object} Fastify plugin options
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
export async function initSpaceRoutes(
|
|
134
|
+
app: FastifyInstance,
|
|
135
|
+
_options: FastifyPluginOptions
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
app.setErrorHandler(handleError)
|
|
138
|
+
|
|
139
|
+
// Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
|
|
140
|
+
// handler can fall back to an access-control policy (e.g. a public Space).
|
|
141
|
+
app.addHook('onRequest', requireAuthHeadersOrPublicRead)
|
|
142
|
+
// Parse the relevant request headers, set the request.zcap parameter
|
|
143
|
+
app.addHook('onRequest', parseAuthHeaders)
|
|
144
|
+
// Capture raw body bytes (JSON/text) so the digest can be recomputed against
|
|
145
|
+
// exactly what the client signed (spec "Request Body Integrity").
|
|
146
|
+
app.addHook('preParsing', captureRawBody)
|
|
147
|
+
// Enforce the Digest header binding: require it covered by the signature and,
|
|
148
|
+
// when the raw body is available, recompute and compare it.
|
|
149
|
+
app.addHook('preValidation', verifyBodyDigest)
|
|
150
|
+
|
|
151
|
+
// Get Space description object
|
|
152
|
+
app.get('/space/:spaceId', SpaceRequest.get)
|
|
153
|
+
|
|
154
|
+
// Update or Create Space by Id (only "no trailing slash" is valid)
|
|
155
|
+
app.put('/space/:spaceId/', redirectStripSlash)
|
|
156
|
+
app.put('/space/:spaceId', SpaceRequest.put)
|
|
157
|
+
|
|
158
|
+
// Delete Space
|
|
159
|
+
app.delete('/space/:spaceId', SpaceRequest.delete)
|
|
160
|
+
|
|
161
|
+
// List Collections for a space (the canonical form has a trailing slash;
|
|
162
|
+
// registered as GET so `GET /space/:spaceId/collections` redirects there
|
|
163
|
+
// rather than falling through to the Collection GET route).
|
|
164
|
+
app.get('/space/:spaceId/collections', redirectAddSlash)
|
|
165
|
+
app.get('/space/:spaceId/collections/', SpaceRequest.listCollections)
|
|
166
|
+
|
|
167
|
+
// Space access-control policy (reserved segment; Fastify routes static
|
|
168
|
+
// segments ahead of the `:collectionId` parameter, so this never collides).
|
|
169
|
+
app.get('/space/:spaceId/policy', PolicyRequest.get)
|
|
170
|
+
app.put('/space/:spaceId/policy', PolicyRequest.put)
|
|
171
|
+
app.delete('/space/:spaceId/policy', PolicyRequest.delete)
|
|
172
|
+
|
|
173
|
+
// Space linkset (RFC9264 policy discovery)
|
|
174
|
+
app.get('/space/:spaceId/linkset', SpaceRequest.linkset)
|
|
175
|
+
|
|
176
|
+
// Space Backends Available (reserved segment; static-beats-parametric routing
|
|
177
|
+
// keeps this ahead of the `:collectionId` parameter in the Collection routes).
|
|
178
|
+
app.get('/space/:spaceId/backends', SpaceRequest.listBackends)
|
|
179
|
+
|
|
180
|
+
// Register / replace / deregister an `external` backend record. Static
|
|
181
|
+
// `backends` beats the parametric `:collectionId` / `:resourceId` of the
|
|
182
|
+
// Collection/Resource route groups, so these resolve correctly (the same
|
|
183
|
+
// mechanism that keeps the GET above and the singular `/backend` working).
|
|
184
|
+
app.post('/space/:spaceId/backends', BackendRequest.post)
|
|
185
|
+
app.put('/space/:spaceId/backends/:backendId', BackendRequest.put)
|
|
186
|
+
app.delete('/space/:spaceId/backends/:backendId', BackendRequest.delete)
|
|
187
|
+
|
|
188
|
+
// Space Quota report (reserved segment; static-beats-parametric routing keeps
|
|
189
|
+
// this ahead of the `:collectionId` parameter). The per-Collection breakdown
|
|
190
|
+
// (spec's `?include=collections`) is opt-in via that query string -- see the
|
|
191
|
+
// handler note on the ZCap query-string limitation.
|
|
192
|
+
app.get('/space/:spaceId/quotas', SpaceRequest.quotas)
|
|
193
|
+
|
|
194
|
+
// Revoke a zcap delegated from this Space (`:revocationId` = the URL-encoded
|
|
195
|
+
// id of the capability being revoked, which is also the request body). Four
|
|
196
|
+
// segments deep, so it shadows no Collection or Resource route.
|
|
197
|
+
app.post(
|
|
198
|
+
'/space/:spaceId/zcaps/revocations/:revocationId',
|
|
199
|
+
RevocationRequest.postSpace
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Add Collection to a Space
|
|
203
|
+
app.post('/space/:spaceId', redirectAddSlash)
|
|
204
|
+
app.post('/space/:spaceId/', SpaceRequest.post)
|
|
205
|
+
|
|
206
|
+
// POST /space/12345/export
|
|
207
|
+
app.post('/space/:spaceId/export', SpaceRequest.export)
|
|
208
|
+
|
|
209
|
+
// POST /space/12345/import
|
|
210
|
+
app.addContentTypeParser('application/x-tar', (_req, body, done) => {
|
|
211
|
+
done(null, body)
|
|
212
|
+
})
|
|
213
|
+
app.post('/space/:spaceId/import', SpaceRequest.import)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Registers Collection routes (get/update/delete a Collection, list its items,
|
|
218
|
+
* add a Resource). Installs the auth hooks and the `handleError` error handler.
|
|
219
|
+
* @param app {import('fastify').FastifyInstance}
|
|
220
|
+
* @param options {object} Fastify plugin options
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
export async function initCollectionRoutes(
|
|
224
|
+
app: FastifyInstance,
|
|
225
|
+
_options: FastifyPluginOptions
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
app.setErrorHandler(handleError)
|
|
228
|
+
|
|
229
|
+
// Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
|
|
230
|
+
// handler can fall back to an access-control policy (e.g. a public Collection).
|
|
231
|
+
app.addHook('onRequest', requireAuthHeadersOrPublicRead)
|
|
232
|
+
// Parse the relevant request headers, set the request.zcap parameter
|
|
233
|
+
app.addHook('onRequest', parseAuthHeaders)
|
|
234
|
+
// Capture raw body bytes (JSON/text) so the digest can be recomputed against
|
|
235
|
+
// exactly what the client signed (spec "Request Body Integrity").
|
|
236
|
+
app.addHook('preParsing', captureRawBody)
|
|
237
|
+
// Enforce the Digest header binding: require it covered by the signature and,
|
|
238
|
+
// when the raw body is available, recompute and compare it.
|
|
239
|
+
app.addHook('preValidation', verifyBodyDigest)
|
|
240
|
+
|
|
241
|
+
// Get Collection description
|
|
242
|
+
app.get('/space/:spaceId/:collectionId', CollectionRequest.get)
|
|
243
|
+
// List Collection items
|
|
244
|
+
app.get('/space/:spaceId/:collectionId/', CollectionRequest.list)
|
|
245
|
+
|
|
246
|
+
// Collection access-control policy (reserved segment; static-beats-parametric
|
|
247
|
+
// routing keeps this ahead of the `:resourceId` parameter).
|
|
248
|
+
app.get('/space/:spaceId/:collectionId/policy', PolicyRequest.get)
|
|
249
|
+
app.put('/space/:spaceId/:collectionId/policy', PolicyRequest.put)
|
|
250
|
+
app.delete('/space/:spaceId/:collectionId/policy', PolicyRequest.delete)
|
|
251
|
+
|
|
252
|
+
// Collection linkset (RFC9264 policy discovery)
|
|
253
|
+
app.get('/space/:spaceId/:collectionId/linkset', CollectionRequest.linkset)
|
|
254
|
+
|
|
255
|
+
// Collection Backend Selected (reserved segment; static-beats-parametric
|
|
256
|
+
// routing keeps this ahead of the `:resourceId` parameter in Resource routes).
|
|
257
|
+
app.get('/space/:spaceId/:collectionId/backend', CollectionRequest.getBackend)
|
|
258
|
+
|
|
259
|
+
// Per-Collection Quota report (reserved segment; static-beats-parametric
|
|
260
|
+
// routing keeps this ahead of the `:resourceId` parameter in Resource routes).
|
|
261
|
+
app.get('/space/:spaceId/:collectionId/quota', CollectionRequest.getQuota)
|
|
262
|
+
|
|
263
|
+
// Collection query (reserved segment; spec "Collection-level reserved
|
|
264
|
+
// endpoints"). The WAS server serves the replication change feed as the
|
|
265
|
+
// `changes` profile; params ride the signed
|
|
266
|
+
// POST body. Static-beats-parametric routing keeps this ahead of the
|
|
267
|
+
// `:resourceId` parameter in Resource routes.
|
|
268
|
+
app.post('/space/:spaceId/:collectionId/query', CollectionRequest.query)
|
|
269
|
+
|
|
270
|
+
// Add Resource to a Collection
|
|
271
|
+
app.post('/space/:spaceId/:collectionId', redirectAddSlash)
|
|
272
|
+
app.post('/space/:spaceId/:collectionId/', CollectionRequest.post)
|
|
273
|
+
|
|
274
|
+
// Create a Collection by Id
|
|
275
|
+
app.put(
|
|
276
|
+
'/space/:spaceId/:collectionId/', // no trailing slash allowed
|
|
277
|
+
redirectStripSlash
|
|
278
|
+
)
|
|
279
|
+
app.put('/space/:spaceId/:collectionId', CollectionRequest.put)
|
|
280
|
+
|
|
281
|
+
// Delete Collection by Id
|
|
282
|
+
app.delete(
|
|
283
|
+
'/space/:spaceId/:collectionId/', // no trailing slash allowed
|
|
284
|
+
redirectStripSlash
|
|
285
|
+
)
|
|
286
|
+
app.delete('/space/:spaceId/:collectionId', CollectionRequest.delete)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Registers Resource routes (create-by-id, get, delete a Resource). Installs the
|
|
291
|
+
* auth hooks and the `handleError` error handler.
|
|
292
|
+
* @param app {import('fastify').FastifyInstance}
|
|
293
|
+
* @param options {object} Fastify plugin options
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
export async function initResourceRoutes(
|
|
297
|
+
app: FastifyInstance,
|
|
298
|
+
_options: FastifyPluginOptions
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
app.setErrorHandler(handleError)
|
|
301
|
+
|
|
302
|
+
// Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
|
|
303
|
+
// handler can fall back to an access-control policy (e.g. a public Resource).
|
|
304
|
+
app.addHook('onRequest', requireAuthHeadersOrPublicRead)
|
|
305
|
+
// Parse the relevant request headers, set the request.zcap parameter
|
|
306
|
+
app.addHook('onRequest', parseAuthHeaders)
|
|
307
|
+
// Capture raw body bytes (JSON/text) so the digest can be recomputed against
|
|
308
|
+
// exactly what the client signed (spec "Request Body Integrity").
|
|
309
|
+
app.addHook('preParsing', captureRawBody)
|
|
310
|
+
// Enforce the Digest header binding: require it covered by the signature and,
|
|
311
|
+
// when the raw body is available, recompute and compare it.
|
|
312
|
+
app.addHook('preValidation', verifyBodyDigest)
|
|
313
|
+
|
|
314
|
+
// Create a Resource by Id
|
|
315
|
+
app.put(
|
|
316
|
+
'/space/:spaceId/:collectionId/:resourceId/', // no trailing slash allowed
|
|
317
|
+
redirectStripSlash
|
|
318
|
+
)
|
|
319
|
+
app.put('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.put)
|
|
320
|
+
|
|
321
|
+
// Head Resource. Declared before the GET route so it overrides Fastify's
|
|
322
|
+
// auto-exposed HEAD (which would share the GET handler and stream the body
|
|
323
|
+
// without a Content-Length); this handler reads only the Metadata and sets
|
|
324
|
+
// Content-Type/Content-Length from it (spec "Content Types and Representations").
|
|
325
|
+
app.head('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.head)
|
|
326
|
+
|
|
327
|
+
// Get Resource
|
|
328
|
+
app.get('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.get)
|
|
329
|
+
|
|
330
|
+
// Delete Resource
|
|
331
|
+
app.delete(
|
|
332
|
+
'/space/:spaceId/:collectionId/:resourceId',
|
|
333
|
+
ResourceRequest.delete
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// Resource access-control policy (reserved segment)
|
|
337
|
+
app.get('/space/:spaceId/:collectionId/:resourceId/policy', PolicyRequest.get)
|
|
338
|
+
app.put('/space/:spaceId/:collectionId/:resourceId/policy', PolicyRequest.put)
|
|
339
|
+
app.delete(
|
|
340
|
+
'/space/:spaceId/:collectionId/:resourceId/policy',
|
|
341
|
+
PolicyRequest.delete
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// Resource metadata (reserved segment; spec "Resource Metadata Data Model")
|
|
345
|
+
app.get(
|
|
346
|
+
'/space/:spaceId/:collectionId/:resourceId/meta',
|
|
347
|
+
ResourceRequest.getMeta
|
|
348
|
+
)
|
|
349
|
+
// Update Resource Metadata (full replacement of the user-writable `custom`).
|
|
350
|
+
app.put(
|
|
351
|
+
'/space/:spaceId/:collectionId/:resourceId/meta',
|
|
352
|
+
ResourceRequest.putMeta
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Registers the WebKMS keystore and key routes (the `/kms` facet).
|
|
358
|
+
* Installs the same hook chain as the WAS groups
|
|
359
|
+
* except that the auth requirement is the strict `requireAuthHeaders`: every
|
|
360
|
+
* webkms route, GETs included, is zcap-invoked -- the protocol has no public
|
|
361
|
+
* reads. No slash-redirect variants either; the protocol's URLs are exact --
|
|
362
|
+
* only these shapes are registered.
|
|
363
|
+
* @param app {import('fastify').FastifyInstance}
|
|
364
|
+
* @param options {object} Fastify plugin options
|
|
365
|
+
* @returns {Promise<void>}
|
|
366
|
+
*/
|
|
367
|
+
export async function initKmsRoutes(
|
|
368
|
+
app: FastifyInstance,
|
|
369
|
+
_options: FastifyPluginOptions
|
|
370
|
+
): Promise<void> {
|
|
371
|
+
app.setErrorHandler(handleError)
|
|
372
|
+
|
|
373
|
+
// Gate provisioning (Create Keystore): the configured policy may grant/deny,
|
|
374
|
+
// or (the default) allow -- in which case the normal zcap path below runs.
|
|
375
|
+
app.addHook('onRequest', provisioningGate)
|
|
376
|
+
// Every operation is privileged: 401 when auth headers are absent.
|
|
377
|
+
app.addHook('onRequest', requireAuthHeaders)
|
|
378
|
+
// Parse the relevant request headers, set the request.zcap parameter
|
|
379
|
+
app.addHook('onRequest', parseAuthHeaders)
|
|
380
|
+
// Capture raw body bytes (JSON/text) so the digest can be recomputed against
|
|
381
|
+
// exactly what the client signed (spec "Request Body Integrity").
|
|
382
|
+
app.addHook('preParsing', captureRawBody)
|
|
383
|
+
// Enforce the Digest header binding: require it covered by the signature and,
|
|
384
|
+
// when the raw body is available, recompute and compare it.
|
|
385
|
+
app.addHook('preValidation', verifyBodyDigest)
|
|
386
|
+
|
|
387
|
+
// Create Keystore
|
|
388
|
+
app.post('/kms/keystores', KeystoreRequest.post)
|
|
389
|
+
|
|
390
|
+
// List Keystores by controller (`?controller=<did>`)
|
|
391
|
+
app.get('/kms/keystores', KeystoreRequest.list)
|
|
392
|
+
|
|
393
|
+
// Get Keystore config
|
|
394
|
+
app.get('/kms/keystores/:keystoreId', KeystoreRequest.get)
|
|
395
|
+
|
|
396
|
+
// Update Keystore config
|
|
397
|
+
app.post('/kms/keystores/:keystoreId', KeystoreRequest.update)
|
|
398
|
+
|
|
399
|
+
// Generate Key (GenerateKeyOperation)
|
|
400
|
+
app.post('/kms/keystores/:keystoreId/keys', KeyRequest.generate)
|
|
401
|
+
|
|
402
|
+
// List Keys (fork extension: enumerate the keystore's public key
|
|
403
|
+
// descriptions). Static `/keys` beats the parametric `/keys/:keyId` below,
|
|
404
|
+
// so this never collides with the key-description GET.
|
|
405
|
+
app.get('/kms/keystores/:keystoreId/keys', KeyRequest.list)
|
|
406
|
+
|
|
407
|
+
// Key operation dispatch by envelope type (Sign / Verify / DeriveSecret /
|
|
408
|
+
// WrapKey / UnwrapKey)
|
|
409
|
+
app.post('/kms/keystores/:keystoreId/keys/:keyId', KeyRequest.operation)
|
|
410
|
+
|
|
411
|
+
// Public key description
|
|
412
|
+
app.get('/kms/keystores/:keystoreId/keys/:keyId', KeyRequest.get)
|
|
413
|
+
|
|
414
|
+
// Revoke a delegated zcap (`:revocationId` = the URL-encoded id of the
|
|
415
|
+
// capability being revoked, which is also the request body)
|
|
416
|
+
app.post(
|
|
417
|
+
'/kms/keystores/:keystoreId/zcaps/revocations/:revocationId',
|
|
418
|
+
RevocationRequest.post
|
|
419
|
+
)
|
|
420
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App factory: createApp() is the community-edition composition. It builds the
|
|
3
|
+
* Fastify instance, registers the `fastifyWas` plugin (the whole WAS protocol
|
|
4
|
+
* surface -- see plugin.ts), and adds the teaching-server extras around it:
|
|
5
|
+
* static assets, the Handlebars-rendered welcome page, the `/health` probe,
|
|
6
|
+
* and the CORS proxy.
|
|
7
|
+
*/
|
|
8
|
+
import Fastify, { type FastifyInstance } from 'fastify'
|
|
9
|
+
import fastifyView from '@fastify/view'
|
|
10
|
+
import fastifyStatic from '@fastify/static'
|
|
11
|
+
import handlebars from 'handlebars'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { fastifyWas, type FastifyWasOptions } from './plugin.js'
|
|
15
|
+
import { initCorsProxyRoutes as initApiCorsProxyRoutes } from './corsProxy.js'
|
|
16
|
+
import { SPEC_URL, SERVER_VERSION } from './config.default.js'
|
|
17
|
+
|
|
18
|
+
// TODO: https://github.com/fastify/fastify-helmet
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds the Fastify instance: registers the `fastifyWas` protocol plugin
|
|
22
|
+
* (which carries the storage/config decorations and all WAS + WebKMS routes;
|
|
23
|
+
* options are passed through -- see {@link FastifyWasOptions}), then the
|
|
24
|
+
* teaching-server extras (static files, welcome page, health probe, CORS
|
|
25
|
+
* proxy).
|
|
26
|
+
* @param options {FastifyWasOptions}
|
|
27
|
+
* @returns {import('fastify').FastifyInstance}
|
|
28
|
+
*/
|
|
29
|
+
export function createApp(options: FastifyWasOptions = {}): FastifyInstance {
|
|
30
|
+
// By default uses 'pino' logger
|
|
31
|
+
const fastify = Fastify({ logger: true })
|
|
32
|
+
|
|
33
|
+
// The WAS protocol surface (decorations, parsers, WAS + WebKMS route groups)
|
|
34
|
+
fastify.register(fastifyWas, options)
|
|
35
|
+
|
|
36
|
+
// Serve static files from the /common folder
|
|
37
|
+
fastify.register(fastifyStatic, {
|
|
38
|
+
root: path.join(import.meta.dirname, '..', 'common'),
|
|
39
|
+
prefix: '/common/'
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Use Handlebars as the rendering engine
|
|
43
|
+
fastify.register(fastifyView, {
|
|
44
|
+
engine: { handlebars },
|
|
45
|
+
root: path.join(import.meta.dirname, './views'),
|
|
46
|
+
layout: '/templates/main', // ./views/templates/main.hbs
|
|
47
|
+
viewExt: 'hbs'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Add a human-readable 'Welcome' page
|
|
51
|
+
fastify.get('/', async (request, reply) => {
|
|
52
|
+
return reply.view('home', { title: 'Welcome', SPEC_URL, SERVER_VERSION })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Operational liveness probe (not a WAS protocol feature). Public,
|
|
56
|
+
// unauthenticated, and side-effect-free so load balancers, uptime monitors,
|
|
57
|
+
// and orchestrators can poll it cheaply. Body follows the
|
|
58
|
+
// `application/health+json` shape from the IETF `draft-inadarei-api-health-check`
|
|
59
|
+
// draft; Fastify's implicit HEAD route serves bodyless probes for free.
|
|
60
|
+
fastify.get('/health', async (request, reply) => {
|
|
61
|
+
return reply
|
|
62
|
+
.type('application/health+json')
|
|
63
|
+
.send({ status: 'pass', version: SERVER_VERSION })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
fastify.register(initApiCorsProxyRoutes)
|
|
67
|
+
|
|
68
|
+
return fastify
|
|
69
|
+
}
|
package/src/start.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point: loads and validates the env config surface via
|
|
3
|
+
* loadConfigFromEnv() (fail-fast on a missing SERVER_URL or any malformed
|
|
4
|
+
* value), builds the app via createApp() and starts listening.
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyInstance } from 'fastify'
|
|
7
|
+
import { createApp } from './server.js'
|
|
8
|
+
import { PostgresBackend } from './backends/postgres.js'
|
|
9
|
+
import { loadConfigFromEnv } from './config.default.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads the validated env config, builds the app via createApp(), and starts
|
|
13
|
+
* listening. Exits the process with code 1 on startup failure.
|
|
14
|
+
* @returns {Promise<void>}
|
|
15
|
+
*/
|
|
16
|
+
export async function startServer(): Promise<void> {
|
|
17
|
+
let fastify: FastifyInstance
|
|
18
|
+
try {
|
|
19
|
+
const config = loadConfigFromEnv()
|
|
20
|
+
// Backend selection: presence of DATABASE_URL selects the Postgres
|
|
21
|
+
// backend; otherwise createApp falls back to the default filesystem
|
|
22
|
+
// backend (rooted at data/). An injected backend carries its own quota
|
|
23
|
+
// configuration, so the per-Space/per-upload limits are passed to it
|
|
24
|
+
// directly rather than through the createApp options.
|
|
25
|
+
const backend = config.databaseUrl
|
|
26
|
+
? new PostgresBackend({
|
|
27
|
+
connectionString: config.databaseUrl,
|
|
28
|
+
capacityBytes: config.storageLimitPerSpace,
|
|
29
|
+
maxUploadBytes: config.maxUploadBytes,
|
|
30
|
+
maxSpacesPerController: config.maxSpacesPerController,
|
|
31
|
+
maxCollectionsPerSpace: config.maxCollectionsPerSpace,
|
|
32
|
+
maxResourcesPerSpace: config.maxResourcesPerSpace
|
|
33
|
+
})
|
|
34
|
+
: undefined
|
|
35
|
+
fastify = createApp({
|
|
36
|
+
serverUrl: config.serverUrl,
|
|
37
|
+
...(backend && { backend }),
|
|
38
|
+
storageLimitPerSpace: config.storageLimitPerSpace,
|
|
39
|
+
maxUploadBytes: config.maxUploadBytes,
|
|
40
|
+
maxSpacesPerController: config.maxSpacesPerController,
|
|
41
|
+
maxCollectionsPerSpace: config.maxCollectionsPerSpace,
|
|
42
|
+
maxResourcesPerSpace: config.maxResourcesPerSpace,
|
|
43
|
+
enabledBackendProviders: config.enabledBackendProviders,
|
|
44
|
+
kmsRecordKek: config.kmsRecordKek,
|
|
45
|
+
onboardingToken: config.onboardingToken
|
|
46
|
+
})
|
|
47
|
+
// Warn (once, at startup, where the Fastify logger now exists) about limits
|
|
48
|
+
// left implicitly unbounded. These warnings live only here so library and
|
|
49
|
+
// test compositions of createApp() stay silent; an explicit `unlimited`
|
|
50
|
+
// (Infinity) or a finite value is a deliberate choice and warns nothing.
|
|
51
|
+
if (config.storageLimitPerSpace === undefined) {
|
|
52
|
+
fastify.log.warn(
|
|
53
|
+
'No per-Space storage quota configured; Spaces may grow without ' +
|
|
54
|
+
'bound. Set STORAGE_LIMIT_PER_SPACE (bytes), or ' +
|
|
55
|
+
'STORAGE_LIMIT_PER_SPACE=unlimited to acknowledge.'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
if (config.maxUploadBytes === Infinity) {
|
|
59
|
+
fastify.log.warn(
|
|
60
|
+
'Per-upload size cap disabled (MAX_UPLOAD_BYTES=unlimited); a single ' +
|
|
61
|
+
'upload may consume unbounded memory on buffered write paths.'
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
await fastify.listen({ port: config.port, host: '0.0.0.0' })
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('Server startup failed:', err)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
startServer()
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage composition helper: builds the default StorageBackend used by the
|
|
3
|
+
* server when no backend is injected.
|
|
4
|
+
*
|
|
5
|
+
* This module only supplies the default backend for production / `start.ts`,
|
|
6
|
+
* where `createApp()` is called without an explicit backend.
|
|
7
|
+
* The active backend is injected into the Fastify instance via
|
|
8
|
+
* `createApp({ backend })` (see plugin.ts) and read in handlers as
|
|
9
|
+
* `request.server.storage`.
|
|
10
|
+
*
|
|
11
|
+
* The StorageBackend contract — including its invariants (getters resolve falsy
|
|
12
|
+
* for not-found, writes are upserts, deletes are idempotent) — is documented on
|
|
13
|
+
* the interface in src/types.ts.
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { FileSystemBackend } from './backends/filesystem.js'
|
|
17
|
+
import type { StorageBackend } from './types.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds the default filesystem-backed storage, rooted at the project `data/`
|
|
21
|
+
* directory. Used by `createApp()` when no backend is injected (production).
|
|
22
|
+
* @param options {object}
|
|
23
|
+
* @param [options.capacityBytes] {number} per-Space storage limit in bytes
|
|
24
|
+
* (spec "Quotas"); `undefined` (or `Infinity`) means each Space is unlimited.
|
|
25
|
+
* @param [options.maxUploadBytes] {number} per-upload size cap in bytes (spec
|
|
26
|
+
* "Quotas", `maxUploadBytes`); `undefined` applies the backend's default-on
|
|
27
|
+
* cap, `Infinity` means no per-upload cap.
|
|
28
|
+
* @param [options.maxSpacesPerController] {number} max Spaces per controller
|
|
29
|
+
* (spec "Quotas"); `undefined` applies the backend's default-on limit,
|
|
30
|
+
* `Infinity` means no cap.
|
|
31
|
+
* @param [options.maxCollectionsPerSpace] {number} max Collections per Space
|
|
32
|
+
* (spec "Quotas"); `undefined` applies the backend's default-on limit,
|
|
33
|
+
* `Infinity` means no cap.
|
|
34
|
+
* @param [options.maxResourcesPerSpace] {number} max live Resources per Space
|
|
35
|
+
* (spec "Quotas"); `undefined` applies the backend's default-on limit,
|
|
36
|
+
* `Infinity` means no cap.
|
|
37
|
+
* @returns {StorageBackend}
|
|
38
|
+
*/
|
|
39
|
+
export function defaultBackend({
|
|
40
|
+
capacityBytes,
|
|
41
|
+
maxUploadBytes,
|
|
42
|
+
maxSpacesPerController,
|
|
43
|
+
maxCollectionsPerSpace,
|
|
44
|
+
maxResourcesPerSpace
|
|
45
|
+
}: {
|
|
46
|
+
capacityBytes?: number
|
|
47
|
+
maxUploadBytes?: number
|
|
48
|
+
maxSpacesPerController?: number
|
|
49
|
+
maxCollectionsPerSpace?: number
|
|
50
|
+
maxResourcesPerSpace?: number
|
|
51
|
+
} = {}): StorageBackend {
|
|
52
|
+
return new FileSystemBackend({
|
|
53
|
+
dataDir: path.join(import.meta.dirname, '..', 'data'),
|
|
54
|
+
capacityBytes,
|
|
55
|
+
maxUploadBytes,
|
|
56
|
+
maxSpacesPerController,
|
|
57
|
+
maxCollectionsPerSpace,
|
|
58
|
+
maxResourcesPerSpace
|
|
59
|
+
})
|
|
60
|
+
}
|