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/plugin.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WAS protocol surface as a registerable Fastify plugin (`fastifyWas`):
|
|
3
|
+
* the storage/config decorations, CORS, multipart and content-type parsers,
|
|
4
|
+
* and the route groups (the four WAS groups plus the WebKMS `/kms` facet) --
|
|
5
|
+
* everything from routes.ts down, including the auth/digest hook chains and
|
|
6
|
+
* the error handler those groups install.
|
|
7
|
+
*
|
|
8
|
+
* The community `createApp()` (server.ts) registers this plugin with defaults;
|
|
9
|
+
* a hardened downstream composition registers the same plugin (with its own
|
|
10
|
+
* backend and policy plugins around it) and inherits the identical wire
|
|
11
|
+
* behavior. Wrapped with `fastify-plugin`, so the decorations and parsers land
|
|
12
|
+
* on the root instance -- while each route group still creates its own
|
|
13
|
+
* encapsulated context for its hooks.
|
|
14
|
+
*/
|
|
15
|
+
import type { FastifyInstance } from 'fastify'
|
|
16
|
+
import fp from 'fastify-plugin'
|
|
17
|
+
import cors from '@fastify/cors'
|
|
18
|
+
import Multipart from '@fastify/multipart'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
initCollectionRoutes,
|
|
22
|
+
initKmsRoutes,
|
|
23
|
+
initResourceRoutes,
|
|
24
|
+
initSpaceRoutes,
|
|
25
|
+
initSpacesRepositoryRoutes
|
|
26
|
+
} from './routes.js'
|
|
27
|
+
import { assertValidServerUrl } from './config.default.js'
|
|
28
|
+
import { defaultBackend } from './storage.js'
|
|
29
|
+
import { onboardingTokenAuthorizer } from './provisioning.js'
|
|
30
|
+
import type {
|
|
31
|
+
StorageBackend,
|
|
32
|
+
BackendProviderRegistry,
|
|
33
|
+
KmsRecordKekRegistry,
|
|
34
|
+
AuthorizeProvisioning
|
|
35
|
+
} from './types.js'
|
|
36
|
+
|
|
37
|
+
export interface FastifyWasOptions {
|
|
38
|
+
/**
|
|
39
|
+
* This server's base URL; used to build and match ZCap invocationTarget URLs
|
|
40
|
+
* (host and port must match exactly). When provided, it must be an absolute
|
|
41
|
+
* `http:`/`https:` URL with no path, query, or fragment (validated at
|
|
42
|
+
* registration -- sub-path deployment is not supported).
|
|
43
|
+
*/
|
|
44
|
+
serverUrl?: string
|
|
45
|
+
/**
|
|
46
|
+
* Persistence backend to use; defaults to a filesystem backend rooted at the
|
|
47
|
+
* project `data/` directory. Tests inject their own (e.g. a
|
|
48
|
+
* FileSystemBackend over a temp dir).
|
|
49
|
+
*/
|
|
50
|
+
backend?: StorageBackend
|
|
51
|
+
/**
|
|
52
|
+
* Per-Space storage limit in bytes (spec "Quotas"); applied only to the
|
|
53
|
+
* default backend (an injected `backend` carries its own `capacityBytes`).
|
|
54
|
+
* `undefined` means unlimited; `Infinity` (an explicit `unlimited`) is
|
|
55
|
+
* normalized by the backend to the same no-limit behavior.
|
|
56
|
+
*/
|
|
57
|
+
storageLimitPerSpace?: number
|
|
58
|
+
/**
|
|
59
|
+
* Per-upload size cap in bytes (spec "Quotas", `maxUploadBytes`); applied
|
|
60
|
+
* only to the default backend (an injected `backend` carries its own).
|
|
61
|
+
* `undefined` applies the backend's default-on cap
|
|
62
|
+
* ({@link DEFAULT_MAX_UPLOAD_BYTES}); `Infinity` (an explicit `unlimited`)
|
|
63
|
+
* disables the cap.
|
|
64
|
+
*/
|
|
65
|
+
maxUploadBytes?: number
|
|
66
|
+
/**
|
|
67
|
+
* Max Spaces a single controller may create (spec "Quotas", a default-on
|
|
68
|
+
* count quota); applied only to the default backend (an injected `backend`
|
|
69
|
+
* carries its own). `undefined` applies the backend's default
|
|
70
|
+
* ({@link DEFAULT_MAX_SPACES_PER_CONTROLLER}); `Infinity` (an explicit
|
|
71
|
+
* `unlimited`) disables the cap.
|
|
72
|
+
*/
|
|
73
|
+
maxSpacesPerController?: number
|
|
74
|
+
/**
|
|
75
|
+
* Max Collections a single Space may hold (spec "Quotas", a default-on count
|
|
76
|
+
* quota); applied only to the default backend (an injected `backend` carries
|
|
77
|
+
* its own). `undefined` applies the backend's default
|
|
78
|
+
* ({@link DEFAULT_MAX_COLLECTIONS_PER_SPACE}); `Infinity` (an explicit
|
|
79
|
+
* `unlimited`) disables the cap.
|
|
80
|
+
*/
|
|
81
|
+
maxCollectionsPerSpace?: number
|
|
82
|
+
/**
|
|
83
|
+
* Max live Resources a single Space may hold across all its Collections (spec
|
|
84
|
+
* "Quotas", a default-on count quota); applied only to the default backend
|
|
85
|
+
* (an injected `backend` carries its own). `undefined` applies the backend's
|
|
86
|
+
* default ({@link DEFAULT_MAX_RESOURCES_PER_SPACE}); `Infinity` (an explicit
|
|
87
|
+
* `unlimited`) disables the cap.
|
|
88
|
+
*/
|
|
89
|
+
maxResourcesPerSpace?: number
|
|
90
|
+
/**
|
|
91
|
+
* The provider-adapter registry the resolver uses to build a Collection's
|
|
92
|
+
* selected external backend; defaults to an empty map (no external backend
|
|
93
|
+
* is operable).
|
|
94
|
+
*/
|
|
95
|
+
providers?: BackendProviderRegistry
|
|
96
|
+
/**
|
|
97
|
+
* The registration allowlist of backend `provider` names; `undefined` means
|
|
98
|
+
* permissive.
|
|
99
|
+
*/
|
|
100
|
+
enabledBackendProviders?: string[]
|
|
101
|
+
/**
|
|
102
|
+
* The at-rest WebKMS key-record encryption registry (config `KMS_RECORD_KEK` /
|
|
103
|
+
* `KMS_RECORD_KEKS` / `KMS_RECORD_CURRENT_KEK`); `undefined` (or
|
|
104
|
+
* `currentKekId: null`) disables encryption -- key records are written
|
|
105
|
+
* plaintext (the teaching default).
|
|
106
|
+
*/
|
|
107
|
+
kmsRecordKek?: KmsRecordKekRegistry
|
|
108
|
+
/**
|
|
109
|
+
* Custom provisioning gate for `POST /spaces/` and `POST /kms/keystores`;
|
|
110
|
+
* receives `{ request }` and returns `'verify'` (normal zcap path), `'grant'`
|
|
111
|
+
* (authorized by the callback -- skip zcap verification), or `'deny'` (403).
|
|
112
|
+
* `undefined` means allow (the teaching default). Mutually exclusive with
|
|
113
|
+
* `onboardingToken`.
|
|
114
|
+
*/
|
|
115
|
+
authorizeProvisioning?: AuthorizeProvisioning
|
|
116
|
+
/**
|
|
117
|
+
* Shared-secret gate for `POST /spaces/` and `POST /kms/keystores` (config
|
|
118
|
+
* `WAS_ONBOARDING_TOKEN`); when set, those two endpoints require an
|
|
119
|
+
* `Authorization: Bearer <token>` header, which then substitutes for zcap
|
|
120
|
+
* verification on that request. `undefined` means disabled (the teaching
|
|
121
|
+
* default). Mutually exclusive with `authorizeProvisioning`.
|
|
122
|
+
*/
|
|
123
|
+
onboardingToken?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decorates the instance with the WAS storage/config surface, registers the
|
|
128
|
+
* protocol-level plugins and content-type parsers, and mounts the route groups.
|
|
129
|
+
* @param fastify {import('fastify').FastifyInstance}
|
|
130
|
+
* @param options {FastifyWasOptions}
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async function wasPlugin(
|
|
134
|
+
fastify: FastifyInstance,
|
|
135
|
+
options: FastifyWasOptions
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const {
|
|
138
|
+
serverUrl,
|
|
139
|
+
backend,
|
|
140
|
+
storageLimitPerSpace,
|
|
141
|
+
maxUploadBytes,
|
|
142
|
+
maxSpacesPerController,
|
|
143
|
+
maxCollectionsPerSpace,
|
|
144
|
+
maxResourcesPerSpace,
|
|
145
|
+
providers,
|
|
146
|
+
enabledBackendProviders,
|
|
147
|
+
kmsRecordKek,
|
|
148
|
+
authorizeProvisioning,
|
|
149
|
+
onboardingToken
|
|
150
|
+
} = options
|
|
151
|
+
|
|
152
|
+
// Fail fast on a malformed base URL: a serverUrl carrying a path, query, or
|
|
153
|
+
// fragment silently breaks every ZCap invocationTarget match and Location
|
|
154
|
+
// header (URL-joins drop the base path), so it is rejected at registration.
|
|
155
|
+
if (serverUrl !== undefined) {
|
|
156
|
+
assertValidServerUrl(serverUrl)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The two provisioning gates are alternative ways to configure the same seam.
|
|
160
|
+
if (authorizeProvisioning && onboardingToken) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'authorizeProvisioning and onboardingToken are mutually exclusive.'
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fastify.decorate('serverUrl', serverUrl as string)
|
|
167
|
+
// Route the backend's diagnostics through the Fastify pino logger (the backend
|
|
168
|
+
// defaults to a silent logger until wired here).
|
|
169
|
+
const storage =
|
|
170
|
+
backend ??
|
|
171
|
+
defaultBackend({
|
|
172
|
+
capacityBytes: storageLimitPerSpace,
|
|
173
|
+
maxUploadBytes,
|
|
174
|
+
maxSpacesPerController,
|
|
175
|
+
maxCollectionsPerSpace,
|
|
176
|
+
maxResourcesPerSpace
|
|
177
|
+
})
|
|
178
|
+
storage.logger = fastify.log
|
|
179
|
+
fastify.decorate('storage', storage)
|
|
180
|
+
|
|
181
|
+
// Backend lifecycle: run the optional startup hook (e.g. Postgres connect +
|
|
182
|
+
// migrations) during registration, before the server starts listening, and
|
|
183
|
+
// wire the optional shutdown hook (pool drain) to Fastify's close.
|
|
184
|
+
if (storage.init) {
|
|
185
|
+
await storage.init()
|
|
186
|
+
}
|
|
187
|
+
if (storage.close) {
|
|
188
|
+
fastify.addHook('onClose', async () => {
|
|
189
|
+
await storage.close!()
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The provider-adapter registry the resolver (lib/backendRegistry.ts) consults
|
|
194
|
+
// to build a Collection's selected external backend. Injected (rather than a
|
|
195
|
+
// module-global mutable registry) so parallel test suites stay isolated -- the
|
|
196
|
+
// same rationale as the injected `storage`. Empty in production this stage.
|
|
197
|
+
fastify.decorate('backendProviders', providers ?? new Map())
|
|
198
|
+
// The optional registration allowlist (config `WAS_ENABLED_BACKENDS`);
|
|
199
|
+
// `undefined` = permissive (any provider may be registered).
|
|
200
|
+
fastify.decorate('enabledBackendProviders', enabledBackendProviders)
|
|
201
|
+
// The at-rest key-record encryption registry (config `KMS_RECORD_KEK`);
|
|
202
|
+
// `undefined` = disabled (records written plaintext). Read at the KMS
|
|
203
|
+
// orchestration seam (KeyRequest), never inside a backend.
|
|
204
|
+
fastify.decorate('kmsRecordKek', kmsRecordKek)
|
|
205
|
+
// The provisioning gate for `POST /spaces/` and `POST /kms/keystores`: a
|
|
206
|
+
// custom callback, or the stock onboarding-token check when a token is set,
|
|
207
|
+
// or `undefined` = allow (the teaching default). Read by the `provisioningGate`
|
|
208
|
+
// onRequest hook installed by those two route groups.
|
|
209
|
+
fastify.decorate(
|
|
210
|
+
'authorizeProvisioning',
|
|
211
|
+
authorizeProvisioning ??
|
|
212
|
+
(onboardingToken ? onboardingTokenAuthorizer(onboardingToken) : undefined)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// Disable CORS
|
|
216
|
+
fastify.register(cors, {
|
|
217
|
+
origin: '*',
|
|
218
|
+
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Multipart file uploading. The cap is `files: 2`, not `1`: a write MUST carry
|
|
222
|
+
// exactly one file part, and `resolveResourceInput` enforces that by iterating
|
|
223
|
+
// the parts and rejecting a second one with `invalid-request-body` (400). With
|
|
224
|
+
// a `files: 1` limit busboy would instead silently drop the second part and
|
|
225
|
+
// raise its own `FST_FILES_LIMIT` (413), so the second part must be allowed
|
|
226
|
+
// through to the iterator to be caught and rejected with the correct error.
|
|
227
|
+
//
|
|
228
|
+
// `fileSize` bounds the in-memory buffer of the single permitted part to the
|
|
229
|
+
// backend's per-upload cap (`throwFileSizeLimit` makes `toBuffer()` throw at
|
|
230
|
+
// the boundary, which the request layer maps to `payload-too-large` (413)) --
|
|
231
|
+
// so an oversize multipart upload is rejected before it is fully buffered.
|
|
232
|
+
// The cap is default-on (the backend applies `DEFAULT_MAX_UPLOAD_BYTES` when
|
|
233
|
+
// none is configured), so `storage.maxUploadBytes` is `undefined` here only
|
|
234
|
+
// when the operator explicitly opted out (`MAX_UPLOAD_BYTES=unlimited`); the
|
|
235
|
+
// conditional spread then leaves multipart uncapped. Large binaries should
|
|
236
|
+
// use the streaming raw-body path, not multipart.
|
|
237
|
+
fastify.register(Multipart, {
|
|
238
|
+
throwFileSizeLimit: true,
|
|
239
|
+
limits: {
|
|
240
|
+
files: 2,
|
|
241
|
+
...(storage.maxUploadBytes !== undefined && {
|
|
242
|
+
fileSize: storage.maxUploadBytes
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Parse `application/<suffix>+json` bodies (e.g. `application/jose+json` for
|
|
248
|
+
// EDV-over-WAS encrypted documents, `application/ld+json`, etc.) as JSON, the
|
|
249
|
+
// same as plain `application/json`. Fastify's built-in JSON parser only
|
|
250
|
+
// matches `application/json` exactly, so structured-suffix JSON media types
|
|
251
|
+
// would otherwise be rejected with a 415. The regex deliberately requires a
|
|
252
|
+
// non-`+` suffix before `+json`, so it never shadows the built-in parser for
|
|
253
|
+
// plain `application/json`. Registered on the root instance so every route
|
|
254
|
+
// group inherits it; `isJson()` already treats `+json` as JSON downstream
|
|
255
|
+
// (digest capture, resource-input resolution).
|
|
256
|
+
fastify.addContentTypeParser(
|
|
257
|
+
/^application\/[^+]+\+json/,
|
|
258
|
+
{ parseAs: 'string' },
|
|
259
|
+
fastify.getDefaultJsonParser('error', 'error')
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
// Catch-all parser for arbitrary binary representations. The spec ("Content
|
|
263
|
+
// Types and Representations") lets a Resource be any media type, so a raw
|
|
264
|
+
// (non-multipart) blob PUT/POST -- `application/octet-stream`,
|
|
265
|
+
// `application/jsonl`, images, etc. -- must reach the handler as a byte
|
|
266
|
+
// stream. Fastify only ships parsers for `application/json` and `text/plain`
|
|
267
|
+
// and would otherwise reject every other media type with a 415 before the
|
|
268
|
+
// route runs. This bare pass-through leaves `request.body` as the raw stream,
|
|
269
|
+
// which `resolveResourceInput` normalizes to a `kind: 'binary'` input that the
|
|
270
|
+
// backend streams straight to storage. More specific parsers still win over
|
|
271
|
+
// this fallback: the built-in JSON/text parsers, the `+json` regex above,
|
|
272
|
+
// `@fastify/multipart`'s `multipart/*`, and the `application/x-tar` import
|
|
273
|
+
// parser are all matched ahead of it.
|
|
274
|
+
fastify.addContentTypeParser('*', function (_request, payload, done) {
|
|
275
|
+
done(null, payload)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
fastify.register(initSpacesRepositoryRoutes)
|
|
279
|
+
fastify.register(initSpaceRoutes)
|
|
280
|
+
fastify.register(initCollectionRoutes)
|
|
281
|
+
fastify.register(initResourceRoutes)
|
|
282
|
+
fastify.register(initKmsRoutes)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const fastifyWas = fp(wasPlugin, {
|
|
286
|
+
fastify: '5.x',
|
|
287
|
+
name: 'fastify-was'
|
|
288
|
+
})
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access-control policy evaluation: resolves the effective policy for a target
|
|
3
|
+
* (most-specific level wins) and decides whether a policy grants an action.
|
|
4
|
+
* Policies are the fallback authorization path -- consulted when a capability
|
|
5
|
+
* invocation is absent or does not grant access (see authorize.ts). v1
|
|
6
|
+
* recognizes only `{ "type": "PublicCanRead" }`; any other `type` grants nothing
|
|
7
|
+
* (fail-closed). Also builds the RFC9264 linkset that advertises a Space's or
|
|
8
|
+
* Collection's policy resource for discovery.
|
|
9
|
+
*/
|
|
10
|
+
import type { FastifyBaseLogger } from 'fastify'
|
|
11
|
+
import type { PolicyDocument, StorageBackend } from './types.js'
|
|
12
|
+
import {
|
|
13
|
+
POLICY_LINK_RELATION,
|
|
14
|
+
BACKEND_LINK_RELATION,
|
|
15
|
+
QUOTA_LINK_RELATION,
|
|
16
|
+
BACKENDS_AVAILABLE_LINK_RELATION,
|
|
17
|
+
QUOTAS_LINK_RELATION
|
|
18
|
+
} from './config.default.js'
|
|
19
|
+
import {
|
|
20
|
+
collectionPath,
|
|
21
|
+
spacePath,
|
|
22
|
+
policyPath,
|
|
23
|
+
backendPath,
|
|
24
|
+
quotaPath,
|
|
25
|
+
backendsPath,
|
|
26
|
+
quotasPath
|
|
27
|
+
} from './lib/paths.js'
|
|
28
|
+
|
|
29
|
+
/** The kind of access a request needs; derived from the HTTP method. */
|
|
30
|
+
export type AccessAction = 'read' | 'write'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the policy that governs a target, honoring the spec's
|
|
34
|
+
* most-specific-wins inheritance: a Resource policy overrides a Collection
|
|
35
|
+
* policy, which overrides a Space policy. The first level that has a policy
|
|
36
|
+
* document is the effective policy; if none do, resolves undefined.
|
|
37
|
+
*
|
|
38
|
+
* @param options {object}
|
|
39
|
+
* @param options.storage {StorageBackend} the request's storage backend
|
|
40
|
+
* @param options.spaceId {string}
|
|
41
|
+
* @param [options.collectionId] {string}
|
|
42
|
+
* @param [options.resourceId] {string}
|
|
43
|
+
* @returns {Promise<PolicyDocument | undefined>}
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveEffectivePolicy({
|
|
46
|
+
storage,
|
|
47
|
+
spaceId,
|
|
48
|
+
collectionId,
|
|
49
|
+
resourceId
|
|
50
|
+
}: {
|
|
51
|
+
storage: StorageBackend
|
|
52
|
+
spaceId: string
|
|
53
|
+
collectionId?: string
|
|
54
|
+
resourceId?: string
|
|
55
|
+
}): Promise<PolicyDocument | undefined> {
|
|
56
|
+
if (collectionId !== undefined && resourceId !== undefined) {
|
|
57
|
+
const resourcePolicy = await storage.getPolicy({
|
|
58
|
+
spaceId,
|
|
59
|
+
collectionId,
|
|
60
|
+
resourceId
|
|
61
|
+
})
|
|
62
|
+
if (resourcePolicy) {
|
|
63
|
+
return resourcePolicy
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (collectionId !== undefined) {
|
|
67
|
+
const collectionPolicy = await storage.getPolicy({ spaceId, collectionId })
|
|
68
|
+
if (collectionPolicy) {
|
|
69
|
+
return collectionPolicy
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return await storage.getPolicy({ spaceId })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decides whether a policy document grants the requested action. Dispatches on
|
|
77
|
+
* the policy `type`; an absent policy or an unrecognized `type` grants nothing
|
|
78
|
+
* (fail-closed).
|
|
79
|
+
*
|
|
80
|
+
* @param options {object}
|
|
81
|
+
* @param [options.policy] {PolicyDocument} the effective policy, if any
|
|
82
|
+
* @param options.action {AccessAction}
|
|
83
|
+
* @param [options.logger] {FastifyBaseLogger} logger for the fail-closed warn
|
|
84
|
+
* on an unrecognized policy type
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
export function policyGrants({
|
|
88
|
+
policy,
|
|
89
|
+
action,
|
|
90
|
+
logger
|
|
91
|
+
}: {
|
|
92
|
+
policy?: PolicyDocument
|
|
93
|
+
action: AccessAction
|
|
94
|
+
logger?: FastifyBaseLogger
|
|
95
|
+
}): boolean {
|
|
96
|
+
if (!policy) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
switch (policy.type) {
|
|
100
|
+
case 'PublicCanRead':
|
|
101
|
+
return action === 'read'
|
|
102
|
+
default:
|
|
103
|
+
// Unknown policy type: grant nothing, fall through to zcap-only decision.
|
|
104
|
+
// Warn so a misconfigured / unsupported policy is diagnosable (the
|
|
105
|
+
// fail-closed behavior itself is correct; this is purely a signal).
|
|
106
|
+
logger?.warn(
|
|
107
|
+
{ policyType: policy.type },
|
|
108
|
+
'Unrecognized access-control policy type; granting nothing (fail-closed).'
|
|
109
|
+
)
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Builds the RFC9264 linkset for a Space or Collection, advertising its
|
|
116
|
+
* auxiliary resources for discovery. Collection vs Space is selected by whether
|
|
117
|
+
* `collectionId` is present.
|
|
118
|
+
*
|
|
119
|
+
* Relations:
|
|
120
|
+
* - `policy` (`POLICY_LINK_RELATION`) -- the access-control policy resource,
|
|
121
|
+
* advertised only when a policy document is set at that exact level.
|
|
122
|
+
* - On a Collection (`collectionId` present): `backend` (`BACKEND_LINK_RELATION`)
|
|
123
|
+
* and `quota` (`QUOTA_LINK_RELATION`), advertised unconditionally (every
|
|
124
|
+
* Collection has a selected backend and a quota report endpoint).
|
|
125
|
+
* - On a Space (`collectionId` absent): `backends-available`
|
|
126
|
+
* (`BACKENDS_AVAILABLE_LINK_RELATION`) and `quotas` (`QUOTAS_LINK_RELATION`),
|
|
127
|
+
* advertised unconditionally (both endpoints always exist).
|
|
128
|
+
*
|
|
129
|
+
* @param options {object}
|
|
130
|
+
* @param options.storage {StorageBackend} the request's storage backend
|
|
131
|
+
* @param options.spaceId {string}
|
|
132
|
+
* @param [options.collectionId] {string}
|
|
133
|
+
* @returns {Promise<object>} a `{ linkset: [...] }` object
|
|
134
|
+
*/
|
|
135
|
+
export async function buildLinkset({
|
|
136
|
+
storage,
|
|
137
|
+
spaceId,
|
|
138
|
+
collectionId
|
|
139
|
+
}: {
|
|
140
|
+
storage: StorageBackend
|
|
141
|
+
spaceId: string
|
|
142
|
+
collectionId?: string
|
|
143
|
+
}): Promise<{ linkset: Array<Record<string, unknown>> }> {
|
|
144
|
+
const anchor =
|
|
145
|
+
collectionId !== undefined
|
|
146
|
+
? collectionPath({ spaceId, collectionId })
|
|
147
|
+
: spacePath({ spaceId })
|
|
148
|
+
const policyHref = policyPath({ spaceId, collectionId })
|
|
149
|
+
const policy =
|
|
150
|
+
collectionId !== undefined
|
|
151
|
+
? await storage.getPolicy({ spaceId, collectionId })
|
|
152
|
+
: await storage.getPolicy({ spaceId })
|
|
153
|
+
|
|
154
|
+
const entry: Record<string, unknown> = { anchor }
|
|
155
|
+
if (policy) {
|
|
156
|
+
entry[POLICY_LINK_RELATION] = [
|
|
157
|
+
{ href: policyHref, type: 'application/json' }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
if (collectionId !== undefined) {
|
|
161
|
+
entry[BACKEND_LINK_RELATION] = [
|
|
162
|
+
{ href: backendPath({ spaceId, collectionId }), type: 'application/json' }
|
|
163
|
+
]
|
|
164
|
+
entry[QUOTA_LINK_RELATION] = [
|
|
165
|
+
{ href: quotaPath({ spaceId, collectionId }), type: 'application/json' }
|
|
166
|
+
]
|
|
167
|
+
} else {
|
|
168
|
+
entry[BACKENDS_AVAILABLE_LINK_RELATION] = [
|
|
169
|
+
{ href: backendsPath({ spaceId }), type: 'application/json' }
|
|
170
|
+
]
|
|
171
|
+
entry[QUOTAS_LINK_RELATION] = [
|
|
172
|
+
{ href: quotasPath({ spaceId }), type: 'application/json' }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
return { linkset: [entry] }
|
|
176
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisioning gate for the two open provisioning endpoints (`POST /spaces/`
|
|
3
|
+
* and `POST /kms/keystores`). By default provisioning is allowed -- anyone may
|
|
4
|
+
* create a Space or keystore by proving control of the controller DID named in
|
|
5
|
+
* the request body (the teaching-server behavior). A deployment that wants to
|
|
6
|
+
* gate provisioning supplies either an `onboardingToken` (a shared secret,
|
|
7
|
+
* checked here by the stock `onboardingTokenAuthorizer`) or a custom
|
|
8
|
+
* `authorizeProvisioning` callback; both flow through the same seam.
|
|
9
|
+
*/
|
|
10
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
11
|
+
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
12
|
+
import type { AuthorizeProvisioning, ProvisioningDecision } from './types.js'
|
|
13
|
+
import {
|
|
14
|
+
InvalidOnboardingTokenError,
|
|
15
|
+
MissingOnboardingTokenError,
|
|
16
|
+
ProvisioningNotAuthorizedError
|
|
17
|
+
} from './errors.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The exact route URLs the provisioning gate acts on. `/spaces` (no trailing
|
|
21
|
+
* slash) is included so a token-authorized request reaches the canonical-slash
|
|
22
|
+
* 308 redirect instead of failing the auth-header check first.
|
|
23
|
+
*/
|
|
24
|
+
const PROVISIONING_ROUTES = new Set(['/spaces', '/spaces/', '/kms/keystores'])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* onRequest hook: the provisioning gate. Runs first in the hook chain of the
|
|
28
|
+
* SpacesRepository and `/kms` route groups. For a `POST` to one of the two
|
|
29
|
+
* provisioning endpoints it consults the configured `authorizeProvisioning`
|
|
30
|
+
* callback: `verify` proceeds to normal zcap verification, `grant` marks the
|
|
31
|
+
* request as provisioning-authorized (skipping zcap verification downstream),
|
|
32
|
+
* and `deny` refuses with a 403. Every other request (and every deployment with
|
|
33
|
+
* no callback configured) passes straight through, so the default zcap path is
|
|
34
|
+
* unchanged.
|
|
35
|
+
* @param request {import('fastify').FastifyRequest}
|
|
36
|
+
* @param reply {import('fastify').FastifyReply}
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
export async function provisioningGate(
|
|
40
|
+
request: FastifyRequest,
|
|
41
|
+
_reply: FastifyReply
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
// Only the two provisioning POST endpoints are gated; everything else passes.
|
|
44
|
+
if (
|
|
45
|
+
request.method !== 'POST' ||
|
|
46
|
+
!PROVISIONING_ROUTES.has(request.routeOptions.url ?? '')
|
|
47
|
+
) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const authorize = request.server.authorizeProvisioning
|
|
51
|
+
// No policy configured: default allow -- the zcap path is unchanged.
|
|
52
|
+
if (!authorize) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
const decision = await authorize({ request })
|
|
56
|
+
if (decision === 'grant') {
|
|
57
|
+
request.provisioningAuthorized = true
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (decision === 'deny') {
|
|
61
|
+
throw new ProvisioningNotAuthorizedError()
|
|
62
|
+
}
|
|
63
|
+
// 'verify': fall through to the normal zcap capability-invocation path.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the stock onboarding-token provisioning authorizer: a shared-secret
|
|
68
|
+
* gate that requires an `Authorization: Bearer <token>` header matching the
|
|
69
|
+
* configured token. A request signed with a zcap `Authorization: Signature ...`
|
|
70
|
+
* also lands here -- with a token configured, provisioning requires the token,
|
|
71
|
+
* full stop. Throws `MissingOnboardingTokenError` (401) when no Bearer token is
|
|
72
|
+
* present and `InvalidOnboardingTokenError` (403) when it does not match; a
|
|
73
|
+
* match returns `grant`.
|
|
74
|
+
* @param onboardingToken {string} the configured shared-secret token
|
|
75
|
+
* @returns {AuthorizeProvisioning}
|
|
76
|
+
*/
|
|
77
|
+
export function onboardingTokenAuthorizer(
|
|
78
|
+
onboardingToken: string
|
|
79
|
+
): AuthorizeProvisioning {
|
|
80
|
+
return async function authorizeWithOnboardingToken({
|
|
81
|
+
request
|
|
82
|
+
}): Promise<ProvisioningDecision> {
|
|
83
|
+
const header = request.headers.authorization
|
|
84
|
+
const match = header?.match(/^Bearer\s+(.+)$/i)
|
|
85
|
+
if (!match) {
|
|
86
|
+
throw new MissingOnboardingTokenError()
|
|
87
|
+
}
|
|
88
|
+
const presented = match[1] as string
|
|
89
|
+
// Timing-safe comparison over fixed-length SHA-256 digests, so mismatched
|
|
90
|
+
// token lengths do not leak through `timingSafeEqual`'s length check.
|
|
91
|
+
const presentedDigest = createHash('sha256').update(presented).digest()
|
|
92
|
+
const expectedDigest = createHash('sha256').update(onboardingToken).digest()
|
|
93
|
+
if (!timingSafeEqual(presentedDigest, expectedDigest)) {
|
|
94
|
+
throw new InvalidOnboardingTokenError()
|
|
95
|
+
}
|
|
96
|
+
return 'grant'
|
|
97
|
+
}
|
|
98
|
+
}
|