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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-agnostic conditional-write precondition evaluation (the
|
|
3
|
+
* `conditional-writes` feature). Both storage backends evaluate `If-Match` /
|
|
4
|
+
* `If-None-Match` against a Resource's current state through these two
|
|
5
|
+
* helpers, so the 412 semantics cannot drift between them. Callers MUST invoke
|
|
6
|
+
* them atomically with the write that follows (under the filesystem backend's
|
|
7
|
+
* per-Resource lock, or inside the Postgres backend's row-locking
|
|
8
|
+
* transaction).
|
|
9
|
+
*/
|
|
10
|
+
import { PreconditionFailedError } from '../errors.js'
|
|
11
|
+
import { formatEtag } from './etag.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Evaluates a content-write (or delete) precondition against a Resource's
|
|
15
|
+
* current state. Throws `PreconditionFailedError` (412) when it is not met.
|
|
16
|
+
* `ifNoneMatch` (`If-None-Match: *`, create-if-absent) takes precedence over
|
|
17
|
+
* `ifMatch` when both are present (RFC9110): the write proceeds only if the
|
|
18
|
+
* Resource is absent. `ifMatch` (update-if-unchanged) requires the Resource to
|
|
19
|
+
* exist with a current ETag equal to the supplied validator.
|
|
20
|
+
* @param options {object}
|
|
21
|
+
* @param options.resourceId {string} for the error detail
|
|
22
|
+
* @param options.exists {boolean} whether the Resource currently exists (a
|
|
23
|
+
* tombstone counts as "does not exist")
|
|
24
|
+
* @param options.currentVersion {number} the Resource's current content
|
|
25
|
+
* `version` (0 for a legacy Resource without one)
|
|
26
|
+
* @param [options.ifMatch] {string} a quoted ETag (`If-Match`)
|
|
27
|
+
* @param [options.ifNoneMatch] {boolean} `If-None-Match: *` (create-if-absent)
|
|
28
|
+
* @returns {void}
|
|
29
|
+
*/
|
|
30
|
+
export function assertWritePrecondition({
|
|
31
|
+
resourceId,
|
|
32
|
+
exists,
|
|
33
|
+
currentVersion,
|
|
34
|
+
ifMatch,
|
|
35
|
+
ifNoneMatch
|
|
36
|
+
}: {
|
|
37
|
+
resourceId: string
|
|
38
|
+
exists: boolean
|
|
39
|
+
currentVersion: number
|
|
40
|
+
ifMatch?: string
|
|
41
|
+
ifNoneMatch?: boolean
|
|
42
|
+
}): void {
|
|
43
|
+
if (ifNoneMatch) {
|
|
44
|
+
if (exists) {
|
|
45
|
+
throw new PreconditionFailedError({
|
|
46
|
+
detail: `Resource '${resourceId}' already exists (If-None-Match: *).`
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (ifMatch === undefined) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// `If-Match` (update-if-unchanged): the Resource must exist and its current
|
|
57
|
+
// ETag must equal the supplied validator.
|
|
58
|
+
if (!exists) {
|
|
59
|
+
throw new PreconditionFailedError({
|
|
60
|
+
detail: `Resource '${resourceId}' does not exist; If-Match cannot be satisfied.`
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const currentEtag = formatEtag(currentVersion)
|
|
64
|
+
if (currentEtag !== ifMatch) {
|
|
65
|
+
throw new PreconditionFailedError({
|
|
66
|
+
detail: `Resource '${resourceId}' ETag ${currentEtag} does not match If-Match ${ifMatch}.`
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Evaluates a Collection Description write precondition against the Collection's
|
|
73
|
+
* current description `version` (the `key-epochs` / conditional-Collection-write
|
|
74
|
+
* feature). Throws `PreconditionFailedError` (412) when the supplied `If-Match`
|
|
75
|
+
* validator does not equal the current description ETag. Only `If-Match`
|
|
76
|
+
* (update-if-unchanged) is supported for Collections; a create through an
|
|
77
|
+
* unconditional PUT is unaffected. MUST be called atomically with the write
|
|
78
|
+
* (under the filesystem backend's per-Collection lock, or inside the Postgres
|
|
79
|
+
* backend's row-locking transaction).
|
|
80
|
+
* @param options {object}
|
|
81
|
+
* @param options.collectionId {string} for the error detail
|
|
82
|
+
* @param options.currentVersion {number} the Collection's current description
|
|
83
|
+
* `version` (0 for a legacy Collection without one, or before its first write)
|
|
84
|
+
* @param [options.ifMatch] {string} a quoted ETag (`If-Match`)
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
export function assertCollectionWritePrecondition({
|
|
88
|
+
collectionId,
|
|
89
|
+
currentVersion,
|
|
90
|
+
ifMatch
|
|
91
|
+
}: {
|
|
92
|
+
collectionId: string
|
|
93
|
+
currentVersion: number
|
|
94
|
+
ifMatch?: string
|
|
95
|
+
}): void {
|
|
96
|
+
if (ifMatch === undefined) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const currentEtag = formatEtag(currentVersion)
|
|
100
|
+
if (currentEtag !== ifMatch) {
|
|
101
|
+
throw new PreconditionFailedError({
|
|
102
|
+
detail: `Collection '${collectionId}' ETag ${currentEtag} does not match If-Match ${ifMatch}.`
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluates a metadata-write (`/meta`) precondition against a Resource's
|
|
109
|
+
* current `metaVersion`. Throws `PreconditionFailedError` (412) when it is not
|
|
110
|
+
* met. `If-None-Match: *` means "only if no metadata has been written yet"
|
|
111
|
+
* (`metaVersion` unset); `If-Match` pins the current `metaVersion` ETag.
|
|
112
|
+
* @param options {object}
|
|
113
|
+
* @param options.resourceId {string} for the error detail
|
|
114
|
+
* @param [options.metaVersion] {number} the current `metaVersion`
|
|
115
|
+
* (`undefined` until the first metadata write)
|
|
116
|
+
* @param [options.ifMatch] {string} a quoted ETag (`If-Match`)
|
|
117
|
+
* @param [options.ifNoneMatch] {boolean} `If-None-Match: *`
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
export function assertMetaWritePrecondition({
|
|
121
|
+
resourceId,
|
|
122
|
+
metaVersion,
|
|
123
|
+
ifMatch,
|
|
124
|
+
ifNoneMatch
|
|
125
|
+
}: {
|
|
126
|
+
resourceId: string
|
|
127
|
+
metaVersion?: number
|
|
128
|
+
ifMatch?: string
|
|
129
|
+
ifNoneMatch?: boolean
|
|
130
|
+
}): void {
|
|
131
|
+
if (ifNoneMatch) {
|
|
132
|
+
if (metaVersion !== undefined) {
|
|
133
|
+
throw new PreconditionFailedError({
|
|
134
|
+
detail: `Resource '${resourceId}' metadata already exists (If-None-Match: *).`
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
} else if (ifMatch !== undefined) {
|
|
138
|
+
const currentEtag = formatEtag(metaVersion ?? 0)
|
|
139
|
+
if (currentEtag !== ifMatch) {
|
|
140
|
+
throw new PreconditionFailedError({
|
|
141
|
+
detail: `Resource '${resourceId}' metadata ETag ${currentEtag} does not match If-Match ${ifMatch}.`
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk resource-filename codec: the single shared home for encoding a
|
|
3
|
+
* Resource's representation file name (`r.<resourceId>.<encodedContentType>.<ext>`)
|
|
4
|
+
* and decoding it back. Kept low-level (no imports from `src/backends/` or
|
|
5
|
+
* `importTar.ts`) so both the filesystem backend and the tar importer can depend
|
|
6
|
+
* on it without an import cycle.
|
|
7
|
+
*/
|
|
8
|
+
import * as mime from 'mime-types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Percent-encodes a filename segment so it carries no literal `.`, the
|
|
12
|
+
* structural delimiter of `r.<resourceId>.<encodedContentType>.<ext>`.
|
|
13
|
+
* `encodeURIComponent` leaves `.` unescaped, so escape it explicitly to `%2E`;
|
|
14
|
+
* `decodeURIComponent` reverses both. This keeps resource ids and content-types
|
|
15
|
+
* that legitimately contain dots (e.g. `index.html`, `application/vnd.api+json`)
|
|
16
|
+
* unambiguously parseable -- without it, a dotted id mis-splits and is read back
|
|
17
|
+
* under the wrong id and content-type. For dot-free segments (the common case)
|
|
18
|
+
* the result is byte-identical to the previous `encodeURIComponent`-only scheme.
|
|
19
|
+
* @param segment {string}
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function encodeFilenameSegment(segment: string): string {
|
|
23
|
+
return encodeURIComponent(segment).replace(/\./g, '%2E')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds the on-disk filename for a resource representation:
|
|
28
|
+
* `r.<resourceId>.<encodedContentType>.<ext>`. Both the `resourceId` and
|
|
29
|
+
* content-type segments are dot-escaped (see {@link encodeFilenameSegment}) so
|
|
30
|
+
* the three `.` separators are the only literal dots.
|
|
31
|
+
* @param options {object}
|
|
32
|
+
* @param options.resourceId {string}
|
|
33
|
+
* @param options.contentType {string}
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function fileNameFor({
|
|
37
|
+
resourceId,
|
|
38
|
+
contentType
|
|
39
|
+
}: {
|
|
40
|
+
resourceId: string
|
|
41
|
+
contentType: string
|
|
42
|
+
}): string {
|
|
43
|
+
const encodedId = encodeFilenameSegment(resourceId)
|
|
44
|
+
const encodedType = encodeFilenameSegment(contentType)
|
|
45
|
+
const extension = mime.extension(contentType) || 'blob'
|
|
46
|
+
return `r.${encodedId}.${encodedType}.${extension}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parses an on-disk resource filename (`r.<resourceId>.<encodedContentType>.<ext>`)
|
|
51
|
+
* back into its components, reversing the dot-escaping {@link fileNameFor}
|
|
52
|
+
* applies. Returns the exact stored content-type (decoded from the filename
|
|
53
|
+
* segment, more reliable than `mime.lookup` on the extension), falling back to
|
|
54
|
+
* the spec default `application/octet-stream` if unparseable.
|
|
55
|
+
* @param fileName {string} the basename of the resource file
|
|
56
|
+
* @returns {{ resourceId: string, contentType: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function parseResourceFileName(fileName: string): {
|
|
59
|
+
resourceId: string
|
|
60
|
+
contentType: string
|
|
61
|
+
} {
|
|
62
|
+
const [, encodedId, encodedType] = fileName.split('.')
|
|
63
|
+
return {
|
|
64
|
+
resourceId: encodedId ? decodeURIComponent(encodedId) : '',
|
|
65
|
+
contentType: encodedType
|
|
66
|
+
? decodeURIComponent(encodedType)
|
|
67
|
+
: 'application/octet-stream'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The zcap revocation chain-inspection hook. Wired into every keystore-rooted
|
|
3
|
+
* and Space-rooted
|
|
4
|
+
* verification as the zcap library's `inspectCapabilityChain` extension
|
|
5
|
+
* point, it fails a verification whose dereferenced chain contains any
|
|
6
|
+
* capability with a stored revocation. Both route families share this hook;
|
|
7
|
+
* they differ only in the scope the storage lookup is keyed on (a keystore or
|
|
8
|
+
* a Space). Also home to the revocation-record file-name codec shared by the
|
|
9
|
+
* filesystem store and both backends' Space exports.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto'
|
|
12
|
+
import type {
|
|
13
|
+
CapabilityChainDetails,
|
|
14
|
+
InspectCapabilityChain
|
|
15
|
+
} from '@interop/zcap'
|
|
16
|
+
import type {
|
|
17
|
+
CapabilitySummary,
|
|
18
|
+
RevocationScope,
|
|
19
|
+
StorageBackend
|
|
20
|
+
} from '../types.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The file name a revocation record is stored (and archived) under:
|
|
24
|
+
* the `(delegator, capabilityId)` unique key folded into a SHA-256 digest,
|
|
25
|
+
* plus `.json`. Both parts are arbitrary-length URIs, so hashing (rather than
|
|
26
|
+
* encoding) keeps the name fixed-width and filesystem-safe. Shared by the
|
|
27
|
+
* filesystem store and both backends' `exportSpace`, so the same record
|
|
28
|
+
* always lands under the same name.
|
|
29
|
+
* @param options {object}
|
|
30
|
+
* @param options.delegator {string} the revoked capability's delegator
|
|
31
|
+
* @param options.capabilityId {string} the revoked capability's id
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function revocationFileName({
|
|
35
|
+
delegator,
|
|
36
|
+
capabilityId
|
|
37
|
+
}: {
|
|
38
|
+
delegator: string
|
|
39
|
+
capabilityId: string
|
|
40
|
+
}): string {
|
|
41
|
+
const digest = createHash('sha256')
|
|
42
|
+
.update(`${delegator}\n${capabilityId}`)
|
|
43
|
+
.digest('hex')
|
|
44
|
+
return `${digest}.json`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the `(capabilityId, delegator)` lookup pairs from a verified,
|
|
49
|
+
* dereferenced capability chain. The root capability is skipped -- root zcaps
|
|
50
|
+
* cannot be revoked -- and so is any link whose verify result carries no
|
|
51
|
+
* delegator (nothing to key a revocation record on).
|
|
52
|
+
*
|
|
53
|
+
* @param options {object}
|
|
54
|
+
* @param options.capabilityChain {object[]} the dereferenced chain (root to
|
|
55
|
+
* tail)
|
|
56
|
+
* @param options.capabilityChainMeta {object[]} the per-capability verify
|
|
57
|
+
* results (the root's entry has a `null` `verifyResult`)
|
|
58
|
+
* @returns {CapabilitySummary[]}
|
|
59
|
+
*/
|
|
60
|
+
export function capabilitySummaries({
|
|
61
|
+
capabilityChain,
|
|
62
|
+
capabilityChainMeta
|
|
63
|
+
}: CapabilityChainDetails): CapabilitySummary[] {
|
|
64
|
+
const summaries: CapabilitySummary[] = []
|
|
65
|
+
for (const [index, capability] of capabilityChain.entries()) {
|
|
66
|
+
// skip the root zcap; it cannot be revoked
|
|
67
|
+
if (index === 0) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const verifyResult = capabilityChainMeta[index]?.verifyResult as {
|
|
71
|
+
results?: Array<{ purposeResult?: { delegator?: { id?: string } } }>
|
|
72
|
+
} | null
|
|
73
|
+
const delegator = verifyResult?.results?.[0]?.purposeResult?.delegator?.id
|
|
74
|
+
if (delegator) {
|
|
75
|
+
summaries.push({ capabilityId: capability.id, delegator })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return summaries
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds the `inspectCapabilityChain` hook for one scope: valid when no
|
|
83
|
+
* delegated capability in the chain has a stored revocation under that
|
|
84
|
+
* keystore or Space (a chain of just the root has nothing to check, so a bare
|
|
85
|
+
* root invocation never reaches the store).
|
|
86
|
+
*
|
|
87
|
+
* @param options {object}
|
|
88
|
+
* @param options.storage {StorageBackend} supplies the revocation store
|
|
89
|
+
* @param options.scope {RevocationScope} the keystore or Space the lookup is
|
|
90
|
+
* keyed on
|
|
91
|
+
* @returns {InspectCapabilityChain}
|
|
92
|
+
*/
|
|
93
|
+
export function revocationChainInspector({
|
|
94
|
+
storage,
|
|
95
|
+
scope
|
|
96
|
+
}: {
|
|
97
|
+
storage: StorageBackend
|
|
98
|
+
scope: RevocationScope
|
|
99
|
+
}): InspectCapabilityChain {
|
|
100
|
+
return async details => {
|
|
101
|
+
const capabilities = capabilitySummaries(details)
|
|
102
|
+
if (capabilities.length === 0) {
|
|
103
|
+
return { valid: true }
|
|
104
|
+
}
|
|
105
|
+
const revoked = await storage.isRevoked({ scope, capabilities })
|
|
106
|
+
if (revoked) {
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
error: new Error(
|
|
110
|
+
'One or more capabilities in the chain have been revoked.'
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { valid: true }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for the `controller` DID supplied in Space Description request
|
|
3
|
+
* bodies. The server's only DID method is `did:key` with Ed25519 keys, whose
|
|
4
|
+
* multibase encoding always begins `z6Mk` (the `0xed01` Ed25519-pub multicodec
|
|
5
|
+
* prefix) followed by base58btc characters. This asserts that shape at the
|
|
6
|
+
* request layer so a malformed or non-`did:key` controller is rejected on the
|
|
7
|
+
* way in, rather than being stored and only failing later at
|
|
8
|
+
* capability-verification time.
|
|
9
|
+
*/
|
|
10
|
+
import { InvalidControllerError } from '../errors.js'
|
|
11
|
+
import type { IDID } from '../types.js'
|
|
12
|
+
|
|
13
|
+
// An Ed25519 `did:key` is `did:key:` + `z6Mk` + a base58btc payload. The
|
|
14
|
+
// base58btc (Bitcoin) alphabet omits `0`, `O`, `I`, and `l`.
|
|
15
|
+
const DID_KEY_ED25519_PATTERN = /^did:key:z6Mk[1-9A-HJ-NP-Za-km-z]+$/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns true when `value` is a syntactically valid Ed25519 `did:key` DID.
|
|
19
|
+
* @param value {unknown}
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isValidController(value: unknown): value is IDID {
|
|
23
|
+
return typeof value === 'string' && DID_KEY_ED25519_PATTERN.test(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Asserts that `controller` is a valid Ed25519 `did:key`, throwing
|
|
28
|
+
* InvalidControllerError (400) otherwise.
|
|
29
|
+
* @param controller {unknown} the `controller` value from the request body
|
|
30
|
+
* @param options {object}
|
|
31
|
+
* @param [options.requestName] {string} request name used in the error title
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export function assertValidController(
|
|
35
|
+
controller: unknown,
|
|
36
|
+
{ requestName }: { requestName?: string } = {}
|
|
37
|
+
): void {
|
|
38
|
+
if (!isValidController(controller)) {
|
|
39
|
+
throw new InvalidControllerError({ requestName })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Id sanitization for path-traversal defense. `spaceId` / `collectionId` /
|
|
3
|
+
* `resourceId` values arrive from URL params, request bodies, and tar-entry
|
|
4
|
+
* names, then flow into filesystem paths (and glob patterns). This validator
|
|
5
|
+
* rejects anything that is not a single, URL-safe path segment -- empty, `.`,
|
|
6
|
+
* `..`, a value containing `/` or `\`, or any character outside the RFC 3986
|
|
7
|
+
* "unreserved" set -- so a malicious id can never escape its parent directory.
|
|
8
|
+
* It also rejects Collection / Resource ids that collide with the spec's
|
|
9
|
+
* Reserved Path Segment Registry (`reserved-id`, 409).
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
InvalidSpaceIdError,
|
|
13
|
+
InvalidCollectionIdError,
|
|
14
|
+
InvalidResourceIdError,
|
|
15
|
+
ReservedIdError
|
|
16
|
+
} from '../errors.js'
|
|
17
|
+
|
|
18
|
+
/** Which kind of id is being validated (selects the thrown error class). */
|
|
19
|
+
export type IdKind = 'space' | 'collection' | 'resource'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* URL-safe id charset: the RFC 3986 "unreserved" characters
|
|
23
|
+
* (ALPHA / DIGIT / `-` / `.` / `_` / `~`). This excludes path separators
|
|
24
|
+
* (`/`, `\`) and every glob metacharacter, so a validated id is safe to use
|
|
25
|
+
* both as a single path segment and inside a glob pattern.
|
|
26
|
+
*/
|
|
27
|
+
const ID_PATTERN = /^[A-Za-z0-9._~-]+$/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether `id` is a single, URL-safe path segment: non-empty, not `.` / `..`,
|
|
31
|
+
* free of path separators, and made only of the RFC 3986 "unreserved" charset
|
|
32
|
+
* ({@link ID_PATTERN}). The shared safety predicate behind `assertValidId` (and
|
|
33
|
+
* `assertValidBackendId` in `lib/backends.ts`); does **not** check the reserved
|
|
34
|
+
* path-segment registry, which is id-kind specific.
|
|
35
|
+
* @param id {string}
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function isUrlSafeSegment(id: string): boolean {
|
|
39
|
+
return (
|
|
40
|
+
typeof id === 'string' &&
|
|
41
|
+
id.length > 0 &&
|
|
42
|
+
id !== '.' &&
|
|
43
|
+
id !== '..' &&
|
|
44
|
+
!id.includes('/') &&
|
|
45
|
+
!id.includes('\\') &&
|
|
46
|
+
ID_PATTERN.test(id)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reserved path segments from the spec's Reserved Path Segment Registry (plus
|
|
52
|
+
* the server's own non-spec `import` endpoint). A client-chosen Collection or
|
|
53
|
+
* Resource id matching one of these would shadow the reserved route at that
|
|
54
|
+
* position (e.g. a Collection named `export` would shadow
|
|
55
|
+
* `/space/{id}/export`), so the spec requires rejecting it with 409
|
|
56
|
+
* `reserved-id`. Space ids have no reserved siblings (`/space/{id}` has no
|
|
57
|
+
* static neighbors), so no set exists for the `space` kind.
|
|
58
|
+
*
|
|
59
|
+
* Exported as the server's authoritative per-kind sets so a client can mirror
|
|
60
|
+
* them rather than hand-maintaining a copy (client #13). NOTE the one known
|
|
61
|
+
* divergence from the pure spec registry: `import` is this server's non-spec
|
|
62
|
+
* tar-import endpoint, so a client mirroring the *spec* registry should omit
|
|
63
|
+
* it. Kept byte-identical to `@interop/storage-core`'s exported registry
|
|
64
|
+
* (locked by a drift-guard test); the local definition keeps the id-safety
|
|
65
|
+
* logic self-contained.
|
|
66
|
+
*/
|
|
67
|
+
export const RESERVED_COLLECTION_IDS = new Set([
|
|
68
|
+
'backends',
|
|
69
|
+
'collections',
|
|
70
|
+
'export',
|
|
71
|
+
'import', // non-spec: this server's tar-import endpoint
|
|
72
|
+
'linkset',
|
|
73
|
+
'policy',
|
|
74
|
+
'query',
|
|
75
|
+
'quotas'
|
|
76
|
+
])
|
|
77
|
+
export const RESERVED_RESOURCE_IDS = new Set([
|
|
78
|
+
'backend',
|
|
79
|
+
'linkset',
|
|
80
|
+
'policy',
|
|
81
|
+
'query',
|
|
82
|
+
'quota'
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Asserts that an id is a single, URL-safe path segment that does not collide
|
|
87
|
+
* with a reserved path segment -- throwing the typed 400 `invalid-id` error
|
|
88
|
+
* matching `kind` for an unsafe id, or the 409 `reserved-id` error for a
|
|
89
|
+
* reserved-segment collision.
|
|
90
|
+
* @param id {string} the id parsed from a URL param, body, or tar entry
|
|
91
|
+
* @param options {object}
|
|
92
|
+
* @param options.kind {IdKind} which id is being validated
|
|
93
|
+
* @param [options.requestName] {string} request name used in the error title
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
export function assertValidId(
|
|
97
|
+
id: string,
|
|
98
|
+
{ kind, requestName }: { kind: IdKind; requestName?: string }
|
|
99
|
+
): void {
|
|
100
|
+
if (!isUrlSafeSegment(id)) {
|
|
101
|
+
switch (kind) {
|
|
102
|
+
case 'collection':
|
|
103
|
+
throw new InvalidCollectionIdError({ requestName })
|
|
104
|
+
case 'resource':
|
|
105
|
+
throw new InvalidResourceIdError({ requestName })
|
|
106
|
+
default:
|
|
107
|
+
throw new InvalidSpaceIdError({ requestName })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Collections and Resources may not take a reserved path-segment name.
|
|
112
|
+
const reserved =
|
|
113
|
+
kind === 'collection'
|
|
114
|
+
? RESERVED_COLLECTION_IDS
|
|
115
|
+
: kind === 'resource'
|
|
116
|
+
? RESERVED_RESOURCE_IDS
|
|
117
|
+
: undefined
|
|
118
|
+
if (reserved?.has(id)) {
|
|
119
|
+
throw new ReservedIdError({ kind, id })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Convenience wrapper that validates whichever of `spaceId` / `collectionId` /
|
|
125
|
+
* `resourceId` are present on a request's params object. Call at the top of a
|
|
126
|
+
* handler, before any storage access.
|
|
127
|
+
* @param ids {object}
|
|
128
|
+
* @param [ids.spaceId] {string}
|
|
129
|
+
* @param [ids.collectionId] {string}
|
|
130
|
+
* @param [ids.resourceId] {string}
|
|
131
|
+
* @param options {object}
|
|
132
|
+
* @param [options.requestName] {string} request name used in the error title
|
|
133
|
+
* @returns {void}
|
|
134
|
+
*/
|
|
135
|
+
export function assertValidIds(
|
|
136
|
+
ids: { spaceId?: string; collectionId?: string; resourceId?: string },
|
|
137
|
+
{ requestName }: { requestName?: string } = {}
|
|
138
|
+
): void {
|
|
139
|
+
if (ids.spaceId !== undefined) {
|
|
140
|
+
assertValidId(ids.spaceId, { kind: 'space', requestName })
|
|
141
|
+
}
|
|
142
|
+
if (ids.collectionId !== undefined) {
|
|
143
|
+
assertValidId(ids.collectionId, { kind: 'collection', requestName })
|
|
144
|
+
}
|
|
145
|
+
if (ids.resourceId !== undefined) {
|
|
146
|
+
assertValidId(ids.resourceId, { kind: 'resource', requestName })
|
|
147
|
+
}
|
|
148
|
+
}
|