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,278 @@
1
+ import net from 'node:net';
2
+ import { lookup as dnsLookup } from 'node:dns/promises';
3
+ import { fetch, Agent } from 'undici';
4
+ const PROXY_TIMEOUT_MS = 10_000;
5
+ const MAX_REDIRECTS = 5;
6
+ /**
7
+ * Upper bound on the response body the proxy will buffer and relay. Caps memory
8
+ * use per proxied request and refuses to relay an oversized upstream response
9
+ * (enforced both from a declared `content-length` and while streaming).
10
+ */
11
+ const PROXY_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
12
+ /**
13
+ * True for an IPv4 address in a private, loopback, link-local, or otherwise
14
+ * non-public range -- the SSRF-sensitive destinations the proxy must refuse
15
+ * (RFC 1918 private space, `127.0.0.0/8` loopback, `169.254.0.0/16` link-local
16
+ * -- which covers the `169.254.169.254` cloud-metadata endpoint -- CGNAT, and
17
+ * multicast/reserved). A syntactically invalid address is treated as blocked.
18
+ * @param ip {string}
19
+ * @returns {boolean}
20
+ */
21
+ function isBlockedIpv4(ip) {
22
+ const parts = ip.split('.').map(Number);
23
+ if (parts.length !== 4 ||
24
+ parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
25
+ return true;
26
+ }
27
+ const [a, b] = parts;
28
+ return (a === 0 || // 0.0.0.0/8 "this network"
29
+ a === 10 || // private
30
+ a === 127 || // loopback
31
+ (a === 169 && b === 254) || // link-local (incl. cloud metadata)
32
+ (a === 172 && b >= 16 && b <= 31) || // private
33
+ (a === 192 && b === 168) || // private
34
+ (a === 100 && b >= 64 && b <= 127) || // CGNAT (RFC 6598)
35
+ (a === 192 && b === 0) || // 192.0.0.0/24 IETF protocol assignments
36
+ a >= 224 // multicast / reserved
37
+ );
38
+ }
39
+ /**
40
+ * True for an IPv6 address in a loopback, unspecified, unique-local (`fc00::/7`),
41
+ * or link-local (`fe80::/10`) range, or an IPv4-mapped/embedded address whose
42
+ * IPv4 form is blocked. A syntactically invalid address is treated as blocked.
43
+ * @param ip {string}
44
+ * @returns {boolean}
45
+ */
46
+ function isBlockedIpv6(ip) {
47
+ const lower = ip.toLowerCase();
48
+ const mapped = lower.match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/);
49
+ if (mapped) {
50
+ return isBlockedIpv4(mapped[1]);
51
+ }
52
+ return (lower === '::' ||
53
+ lower === '::1' ||
54
+ lower.startsWith('fe8') || // fe80::/10 link-local
55
+ lower.startsWith('fe9') ||
56
+ lower.startsWith('fea') ||
57
+ lower.startsWith('feb') ||
58
+ lower.startsWith('fc') || // fc00::/7 unique-local
59
+ lower.startsWith('fd'));
60
+ }
61
+ /**
62
+ * True for an IP the proxy must not reach. Unparseable input is blocked
63
+ * defensively.
64
+ * @param ip {string}
65
+ * @returns {boolean}
66
+ */
67
+ function isBlockedIp(ip) {
68
+ const family = net.isIP(ip);
69
+ if (family === 4) {
70
+ return isBlockedIpv4(ip);
71
+ }
72
+ if (family === 6) {
73
+ return isBlockedIpv6(ip);
74
+ }
75
+ return true;
76
+ }
77
+ /**
78
+ * Parses and validates a single proxy target: `http`/`https` scheme only, and
79
+ * the host must not resolve to a blocked (private / loopback / link-local)
80
+ * address. Runs once for the client-supplied URL and again for every redirect
81
+ * hop, so an allowed public host cannot bounce the proxy to an internal one.
82
+ * Returns the exact addresses it validated so the caller can pin the upstream
83
+ * connection to them (closing the DNS-rebinding TOCTOU).
84
+ * @param target {string}
85
+ * @returns {Promise<{url: URL, addresses: {address: string, family: number}[]} | {status: number, error: string}>}
86
+ * the parsed URL plus its validated addresses when allowed, or the HTTP
87
+ * status + message to reject with.
88
+ */
89
+ async function checkProxyTarget(target) {
90
+ let parsed;
91
+ try {
92
+ parsed = new URL(target);
93
+ }
94
+ catch {
95
+ return { status: 400, error: 'Invalid url query parameter' };
96
+ }
97
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
98
+ return {
99
+ status: 400,
100
+ error: 'Only http and https URLs may be proxied.'
101
+ };
102
+ }
103
+ // Resolve the host and refuse private / loopback / link-local destinations
104
+ // (SSRF). `dns.lookup` returns the literal itself for an IP host, so
105
+ // IP-literal targets are covered too.
106
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
107
+ let addresses;
108
+ try {
109
+ addresses = await dnsLookup(hostname, { all: true });
110
+ }
111
+ catch {
112
+ return { status: 502, error: 'Unable to resolve proxied host.' };
113
+ }
114
+ if (addresses.length === 0 ||
115
+ addresses.some(({ address }) => isBlockedIp(address))) {
116
+ return { status: 403, error: 'Proxying to this host is not allowed.' };
117
+ }
118
+ return { url: parsed, addresses };
119
+ }
120
+ /**
121
+ * Builds the undici `connect.lookup` function that pins each upstream socket to
122
+ * the exact addresses `checkProxyTarget` already validated, keyed by hostname.
123
+ * The `fetch` still connects using the original hostname (so TLS certificate
124
+ * validation and SNI keep working) but resolves it only via this map, so a
125
+ * rebinding attacker cannot swap in a fresh, private address between validation
126
+ * and connection. A hostname absent from the map is refused (defense in depth:
127
+ * the agent must never resolve an unpinned host).
128
+ * @param pins {Map<string, {address: string, family: number}[]>} validated
129
+ * addresses keyed by lower-cased hostname.
130
+ * @returns {LookupFunction} an undici/`net`-compatible lookup callback.
131
+ */
132
+ export function createPinnedLookup(pins) {
133
+ return function pinnedLookup(hostname, options, callback) {
134
+ const entries = pins.get(hostname.toLowerCase());
135
+ if (!entries || entries.length === 0) {
136
+ callback(new Error(`Refusing to resolve unpinned host: ${hostname}`));
137
+ return;
138
+ }
139
+ if (options.all) {
140
+ callback(null, entries);
141
+ return;
142
+ }
143
+ const first = entries[0];
144
+ callback(null, first.address, first.family);
145
+ };
146
+ }
147
+ /**
148
+ * Registers a server-side CORS proxy at `/api/cors`.
149
+ *
150
+ * Example:
151
+ * `/api/cors?url=https%3A%2F%2Fregistry.dcconsortium.org%2F.well-known%2Fopenid-federation`
152
+ *
153
+ * SSRF guard: only `http`/`https` URLs are proxied, and the target host is
154
+ * resolved and rejected when it maps to a private, loopback, or link-local
155
+ * address (e.g. `http://169.254.169.254/...` cloud-metadata, or an internal
156
+ * service). Redirects are followed manually (up to `MAX_REDIRECTS` hops) and
157
+ * every hop is re-validated the same way, so a public host cannot 3xx the
158
+ * proxy into an internal one. The addresses each hop validated are then pinned
159
+ * for the actual upstream connection (via a per-request undici `Agent` whose
160
+ * `connect.lookup` resolves only from that pin map), so a DNS-rebinding
161
+ * attacker cannot pass validation with a public IP and have the subsequent
162
+ * `fetch` re-resolve to a private one (closing the TOCTOU). The relayed
163
+ * response body is capped at `PROXY_MAX_RESPONSE_BYTES`, both from a declared
164
+ * `content-length` and while streaming. This is an unauthenticated open
165
+ * endpoint; adding a lightweight auth gate (a shared secret header, an
166
+ * allowlist of caller origins, or the server's own capability-invocation
167
+ * check) is a reasonable follow-up.
168
+ *
169
+ * @param app - Fastify instance
170
+ * @param _options - Fastify plugin options
171
+ */
172
+ export async function initCorsProxyRoutes(app, _options) {
173
+ app.get('/api/cors', async (request, reply) => {
174
+ const target = request.query.url;
175
+ if (!target) {
176
+ return reply.code(400).send({ error: 'Missing url query parameter' });
177
+ }
178
+ const checked = await checkProxyTarget(target);
179
+ if (!('url' in checked)) {
180
+ return reply.code(checked.status).send({ error: checked.error });
181
+ }
182
+ let current = checked.url;
183
+ const headers = {};
184
+ if (typeof request.headers.accept === 'string') {
185
+ headers.accept = request.headers.accept;
186
+ }
187
+ // One undici Agent per proxied request, resolving each hop only via the
188
+ // addresses that hop already validated. The Agent must stay alive until
189
+ // the body is fully read, so it is torn down once in the finally below.
190
+ const pins = new Map();
191
+ const pinKey = (url) => url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
192
+ pins.set(pinKey(current), checked.addresses);
193
+ const agent = new Agent({ connect: { lookup: createPinnedLookup(pins) } });
194
+ try {
195
+ for (let hop = 0;; hop++) {
196
+ const upstream = await fetch(current.href, {
197
+ headers,
198
+ redirect: 'manual',
199
+ signal: AbortSignal.timeout(PROXY_TIMEOUT_MS),
200
+ dispatcher: agent
201
+ });
202
+ const location = upstream.headers.get('location');
203
+ if (upstream.status >= 300 && upstream.status < 400 && location) {
204
+ if (hop >= MAX_REDIRECTS) {
205
+ return reply
206
+ .code(502)
207
+ .send({ error: 'Too many proxied redirects.' });
208
+ }
209
+ let next;
210
+ try {
211
+ next = new URL(location, current);
212
+ }
213
+ catch {
214
+ return reply
215
+ .code(502)
216
+ .send({ error: 'Invalid redirect from proxied URL.' });
217
+ }
218
+ const nextChecked = await checkProxyTarget(next.toString());
219
+ if (!('url' in nextChecked)) {
220
+ return reply
221
+ .code(nextChecked.status)
222
+ .send({ error: nextChecked.error });
223
+ }
224
+ current = nextChecked.url;
225
+ pins.set(pinKey(current), nextChecked.addresses);
226
+ continue;
227
+ }
228
+ // Refuse an upstream that declares a body larger than the cap before
229
+ // reading any of it.
230
+ const declaredLength = Number(upstream.headers.get('content-length'));
231
+ if (Number.isFinite(declaredLength) &&
232
+ declaredLength > PROXY_MAX_RESPONSE_BYTES) {
233
+ return reply
234
+ .code(502)
235
+ .send({ error: 'Proxied response too large.' });
236
+ }
237
+ // Read incrementally so an undeclared (or lying) oversized body is
238
+ // stopped mid-stream rather than fully buffered. Upstream headers
239
+ // are copied only after the body is fully read, so a mid-stream
240
+ // rejection does not carry the upstream's content-type /
241
+ // content-length on the JSON error response.
242
+ const chunks = [];
243
+ if (upstream.body) {
244
+ const reader = upstream.body.getReader();
245
+ let total = 0;
246
+ for (;;) {
247
+ const { done, value } = await reader.read();
248
+ if (done) {
249
+ break;
250
+ }
251
+ total += value.byteLength;
252
+ if (total > PROXY_MAX_RESPONSE_BYTES) {
253
+ await reader.cancel();
254
+ return reply
255
+ .code(502)
256
+ .send({ error: 'Proxied response too large.' });
257
+ }
258
+ chunks.push(Buffer.from(value));
259
+ }
260
+ }
261
+ upstream.headers.forEach((value, name) => reply.header(name, value));
262
+ return reply.code(upstream.status).send(Buffer.concat(chunks));
263
+ }
264
+ }
265
+ catch (error) {
266
+ request.log.warn({ error, target }, 'CORS proxy fetch failed');
267
+ return reply.code(502).send({ error: 'Unable to fetch proxied URL' });
268
+ }
269
+ finally {
270
+ // destroy(), not close(): early-exit paths (blocked redirect, too
271
+ // many redirects, oversized content-length) leave upstream bodies
272
+ // unconsumed, and close() would wait on them. Everything the reply
273
+ // sends is already buffered, so tearing down the sockets is safe.
274
+ await agent.destroy();
275
+ }
276
+ });
277
+ }
278
+ //# sourceMappingURL=corsProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corsProxy.js","sourceRoot":"","sources":["../src/corsProxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Request Body Integrity (spec "Request Body Integrity (Digest Header)"): binds
3
+ * a request body to its HTTP Signature via the `Digest` header. Two hooks,
4
+ * installed by every route group alongside the auth hooks:
5
+ *
6
+ * - `captureRawBody` (preParsing) tees the exact body bytes onto
7
+ * `request.rawBody` for JSON/text bodies, so the digest can be recomputed
8
+ * against what the client actually signed. Re-serializing the parsed body is
9
+ * not guaranteed byte-identical, so we keep the raw bytes instead. Streamed
10
+ * bodies (multipart uploads, tar imports, raw blobs) are not buffered; instead
11
+ * they pass through a hashing transform that verifies the `Digest`
12
+ * incrementally at end-of-stream, so large/binary uploads keep streaming yet
13
+ * are still bound to the signature.
14
+ * - `verifyBodyDigest` (preValidation) enforces, for any request carrying a
15
+ * `Content-Type`: that the signature covers the `digest` header (MUST), that a
16
+ * `Digest` header is present, and -- when the raw body was captured --
17
+ * independently recomputes the body digest and compares it (SHOULD). A
18
+ * missing, malformed, uncovered, or non-matching digest is rejected with
19
+ * `invalid-authorization-header` (400).
20
+ */
21
+ import type { FastifyReply, FastifyRequest } from 'fastify';
22
+ import { type Readable } from 'node:stream';
23
+ /**
24
+ * preParsing hook: for JSON/text bodies, tees the incoming payload into
25
+ * `request.rawBody` while passing the same bytes through to Fastify's content-
26
+ * type parser (so `verifyBodyDigest` can recompute the digest). Other (streamed)
27
+ * bodies are not buffered; when the request is signed with a `Digest` they pass
28
+ * through a `DigestVerifyStream` that verifies that digest incrementally, so a
29
+ * swapped binary/large body under a valid signed `Digest` is still rejected.
30
+ * @param request {import('fastify').FastifyRequest}
31
+ * @param reply {import('fastify').FastifyReply}
32
+ * @param payload {Readable} the raw request body stream
33
+ * @returns {Promise<Readable>} the stream Fastify should parse
34
+ */
35
+ export declare function captureRawBody(request: FastifyRequest, _reply: FastifyReply, payload: Readable): Promise<Readable>;
36
+ /**
37
+ * preValidation hook: enforces the `Digest` header binding for any request that
38
+ * carries a `Content-Type`. Bodyless requests (no `Content-Type`) and anonymous
39
+ * reads (no parsed `request.zcap`) are passed through.
40
+ * @param request {import('fastify').FastifyRequest}
41
+ * @param reply {import('fastify').FastifyReply}
42
+ * @returns {Promise<void>}
43
+ */
44
+ export declare function verifyBodyDigest(request: FastifyRequest, _reply: FastifyReply): Promise<void>;
45
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAyFnE;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,QAAQ,CAAC,CA0BnB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAwDf"}
package/dist/digest.js ADDED
@@ -0,0 +1,174 @@
1
+ import { verifyHeaderValue } from '@interop/http-digest-header';
2
+ import { PassThrough, Transform } from 'node:stream';
3
+ import { createHash } from 'node:crypto';
4
+ import { isJson } from './lib/isJson.js';
5
+ import { InvalidDigestError } from './errors.js';
6
+ /**
7
+ * True for the body shapes `verifyBodyDigest` recomputes: JSON
8
+ * (`application/json` / `+json`) and text. These reach handlers fully parsed
9
+ * (object / string), so buffering their bytes is cheap; streamed bodies
10
+ * (multipart, `application/x-tar`) are deliberately excluded.
11
+ * @param contentType {string | undefined}
12
+ * @returns {boolean}
13
+ */
14
+ function isBufferableBody(contentType) {
15
+ return (isJson({ contentType }) ||
16
+ (typeof contentType === 'string' && contentType.startsWith('text/')));
17
+ }
18
+ /**
19
+ * Compares an incrementally-computed SHA-256 digest against a `Digest` header
20
+ * value, mirroring `@interop/http-digest-header`'s encodings without buffering
21
+ * the body: the multihash form (`mh=u<base64url(0x12 0x20 <digest>)>`, what WAS
22
+ * clients send) and the RFC 9651 `SHA-256=<base64>` form (colons stripped).
23
+ * @param options {object}
24
+ * @param options.sha256Digest {Buffer} the computed 32-byte SHA-256 digest
25
+ * @param options.headerValue {string} the request's `Digest` header value
26
+ * @returns {boolean}
27
+ */
28
+ function digestMatches({ sha256Digest, headerValue }) {
29
+ const [key = '', rawValue] = headerValue.split(/=(.+)/);
30
+ if (key === 'mh') {
31
+ const multihash = Buffer.concat([Buffer.from([0x12, 0x20]), sha256Digest]);
32
+ return rawValue === `u${multihash.toString('base64url')}`;
33
+ }
34
+ if (key.replace('-', '').toLowerCase() === 'sha256') {
35
+ const expected = (rawValue ?? '').replace(/^:(.*):$/, '$1');
36
+ return expected === sha256Digest.toString('base64');
37
+ }
38
+ // Only SHA-256 is supported (as in the library); anything else cannot match.
39
+ return false;
40
+ }
41
+ /**
42
+ * A pass-through Transform that hashes the streamed bytes (SHA-256) and, at
43
+ * end-of-stream, verifies the accumulated digest against the request's `Digest`
44
+ * header -- erroring the stream (which fails the write) on a mismatch. This
45
+ * binds a streamed/binary body to its signed `Digest` without buffering it. A
46
+ * mismatch surfaces mid-write; consumers that persist the stream remove any
47
+ * partial output on the resulting error (see the filesystem backend's blob
48
+ * write).
49
+ */
50
+ class DigestVerifyStream extends Transform {
51
+ _hash = createHash('sha256');
52
+ _digestHeader;
53
+ constructor(digestHeader) {
54
+ super();
55
+ this._digestHeader = digestHeader;
56
+ }
57
+ _transform(chunk, _encoding, callback) {
58
+ this._hash.update(chunk);
59
+ callback(null, chunk);
60
+ }
61
+ _flush(callback) {
62
+ const matched = digestMatches({
63
+ sha256Digest: this._hash.digest(),
64
+ headerValue: this._digestHeader
65
+ });
66
+ callback(matched
67
+ ? null
68
+ : new InvalidDigestError({
69
+ detail: 'The `Digest` header does not match the request body.'
70
+ }));
71
+ }
72
+ }
73
+ /**
74
+ * preParsing hook: for JSON/text bodies, tees the incoming payload into
75
+ * `request.rawBody` while passing the same bytes through to Fastify's content-
76
+ * type parser (so `verifyBodyDigest` can recompute the digest). Other (streamed)
77
+ * bodies are not buffered; when the request is signed with a `Digest` they pass
78
+ * through a `DigestVerifyStream` that verifies that digest incrementally, so a
79
+ * swapped binary/large body under a valid signed `Digest` is still rejected.
80
+ * @param request {import('fastify').FastifyRequest}
81
+ * @param reply {import('fastify').FastifyReply}
82
+ * @param payload {Readable} the raw request body stream
83
+ * @returns {Promise<Readable>} the stream Fastify should parse
84
+ */
85
+ export async function captureRawBody(request, _reply, payload) {
86
+ if (!isBufferableBody(request.headers['content-type'])) {
87
+ // Streamed (non-buffered) body. Bind it to the signature by hashing it as it
88
+ // flows; an unsigned/anonymous body (no `Digest`) has nothing to verify.
89
+ // `parseAuthHeaders` (onRequest) has already run, so `request.zcap` is set
90
+ // for a signed request.
91
+ const digestHeader = request.zcap?.digest;
92
+ if (!digestHeader) {
93
+ return payload;
94
+ }
95
+ const verify = new DigestVerifyStream(digestHeader);
96
+ payload.on('error', err => verify.destroy(err));
97
+ return payload.pipe(verify);
98
+ }
99
+ const chunks = [];
100
+ const passthrough = new PassThrough();
101
+ payload.on('data', chunk => {
102
+ chunks.push(chunk);
103
+ passthrough.write(chunk);
104
+ });
105
+ payload.on('end', () => {
106
+ request.rawBody = Buffer.concat(chunks);
107
+ passthrough.end();
108
+ });
109
+ payload.on('error', err => passthrough.destroy(err));
110
+ return passthrough;
111
+ }
112
+ /**
113
+ * preValidation hook: enforces the `Digest` header binding for any request that
114
+ * carries a `Content-Type`. Bodyless requests (no `Content-Type`) and anonymous
115
+ * reads (no parsed `request.zcap`) are passed through.
116
+ * @param request {import('fastify').FastifyRequest}
117
+ * @param reply {import('fastify').FastifyReply}
118
+ * @returns {Promise<void>}
119
+ */
120
+ export async function verifyBodyDigest(request, _reply) {
121
+ // A provisioning-authorized request (onboarding token) is not signed, so
122
+ // there is no signature to cover a `Digest` header -- nothing to bind.
123
+ if (request.provisioningAuthorized) {
124
+ return;
125
+ }
126
+ const contentType = request.headers['content-type'];
127
+ // Bodyless requests carry no Content-Type and no Digest -- nothing to bind.
128
+ if (!contentType) {
129
+ return;
130
+ }
131
+ // No parsed auth headers: writes require auth (the auth hooks 401 first), so a
132
+ // bodied request reaching here without `zcap` is a safe method that happens to
133
+ // carry a Content-Type; leave it to the handler's policy decision.
134
+ const { zcap } = request;
135
+ if (!zcap) {
136
+ return;
137
+ }
138
+ // MUST: `digest` is among the signature's covered (signed) headers. Guard the
139
+ // `headers` param itself: an `Authorization` header that omits `headers="..."`
140
+ // leaves it undefined, and splitting that would be a 500 rather than the
141
+ // intended `invalid-authorization-header` (400).
142
+ if (!zcap.headers || !zcap.headers.split(/\s+/).includes('digest')) {
143
+ throw new InvalidDigestError({
144
+ detail: 'The request signature must cover the `digest` header when the ' +
145
+ 'request carries a body.'
146
+ });
147
+ }
148
+ // The `Digest` header itself MUST be present.
149
+ if (!zcap.digest) {
150
+ throw new InvalidDigestError({
151
+ detail: 'A `Digest` header is required when the request carries a body.'
152
+ });
153
+ }
154
+ // SHOULD: independently recompute and compare, when the raw body was captured.
155
+ // Streamed bodies (multipart, tar import, raw blobs) are not buffered here;
156
+ // `captureRawBody` has already wrapped them in a `DigestVerifyStream` that
157
+ // verifies the digest incrementally as the body is consumed.
158
+ if (request.rawBody === undefined) {
159
+ return;
160
+ }
161
+ const { verified, error } = await verifyHeaderValue({
162
+ data: request.rawBody,
163
+ headerValue: zcap.digest
164
+ });
165
+ if (!verified) {
166
+ throw new InvalidDigestError({
167
+ detail: error
168
+ ? 'The `Digest` header is malformed or uses an unsupported algorithm.'
169
+ : 'The `Digest` header does not match the request body.',
170
+ cause: error
171
+ });
172
+ }
173
+ }
174
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"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"}