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.
Files changed (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. 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
+ }