hyper-http-fetch 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mauve Signweaver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # hyper-http-fetch
2
+
3
+ Use the fetch API to access HTTP servers exposed by hypertele.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install hyper-http-fetch
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Client: Making requests
14
+
15
+ Import `makeHyperHTTPFetch` to get a `fetch`-compatible function for requesting `hyper+http://` URLs:
16
+
17
+ ```js
18
+ import makeHyperHTTPFetch from "hyper-http-fetch";
19
+
20
+ const fetch = await makeHyperHTTPFetch({
21
+ bootstrap: ["list", "of", "dht", "nodes"], // optional DHT options
22
+ });
23
+
24
+ const res = await fetch("hyper+http://${z32 encoded public key}/path");
25
+ console.log(res.ok);
26
+ console.log(await res.text());
27
+
28
+ // Clean up when done
29
+ fetch.close();
30
+ ```
31
+
32
+ You can also pass an existing `HyperDHT` instance instead of options:
33
+
34
+ ```js
35
+ import HyperDHT from "hyperdht";
36
+ import makeHyperHTTPFetch from "hyper-http-fetch";
37
+
38
+ const dht = new HyperDHT();
39
+ const fetch = await makeHyperHTTPFetch(dht);
40
+
41
+ const res = await fetch("hyper+http://...");
42
+ ```
43
+
44
+ A `Request` object can also be passed as the first argument:
45
+
46
+ ```js
47
+ const res = await fetch(
48
+ new Request("hyper+http://.../path", { method: "POST", body: JSON.stringify({ hello: true }) })
49
+ );
50
+ ```
51
+
52
+ ### Server: Exposing an HTTP server over the DHT
53
+
54
+ Import `./server` to create an HTTP server accessible via `hyper+http://`:
55
+
56
+ ```js
57
+ import createServer from "hyper-http-fetch/server";
58
+
59
+ const server = await createServer((req) => {
60
+ return new Response("Hello World");
61
+ });
62
+
63
+ console.log(server.url); // e.g. "hyper+http://abcdef123.../"
64
+ ```
65
+
66
+ The returned server object has:
67
+
68
+ - **`url`** — the `hyper+http://` URL clients can use to reach this server
69
+ - **`keyPair`** — `{ publicKey, privateKey }` used for the DHT identity
70
+ - **`destroy()`** — shut down the server and clean up
71
+
72
+ You can pass optional configuration:
73
+
74
+ ```js
75
+ const server = await createServer(
76
+ (req) => new Response(req.url),
77
+ {
78
+ dht: { bootstrap: ["my.dht.peer:2000"] },
79
+ seed: crypto.randomBytes(32), // optional, for deterministic keys
80
+ }
81
+ );
82
+ ```
@@ -0,0 +1,6 @@
1
+ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
2
+
3
+ export default [
4
+ // Any other config imports go at the top
5
+ eslintPluginPrettierRecommended,
6
+ ];
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "hyper-http-fetch",
3
+ "version": "1.0.0",
4
+ "description": "Use the fetch API to access HTTP servers exposed by hypertele.",
5
+ "keywords": [
6
+ "http",
7
+ "p2p",
8
+ "fetch",
9
+ "hyperswarm"
10
+ ],
11
+ "homepage": "https://github.com/RangerMauve/hyper-http-fetch#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/RangerMauve/hyper-http-fetch/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/RangerMauve/hyper-http-fetch.git"
18
+ },
19
+ "license": "MIT",
20
+ "author": "RangerMauve",
21
+ "type": "module",
22
+ "main": "src/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "types":"./dist/index.d.ts",
26
+ "default": "./src/index.js"
27
+ },
28
+ "./server": {
29
+ "types": "./dist/server.d.ts",
30
+ "default": "./src/server.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "test": "node --test",
35
+ "lint": "eslint --fix && tsc"
36
+ },
37
+ "dependencies": {
38
+ "hypercore-id-encoding": "^1.3.0",
39
+ "hyperdht": "^6.32.0",
40
+ "undici": "^8.3.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^25.9.1",
44
+ "eslint": "^10.4.1",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-plugin-prettier": "^5.5.6",
47
+ "hypertele": "^1.1.4",
48
+ "prettier": "^3.8.3",
49
+ "typescript": "^6.0.3"
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,60 @@
1
+ import {
2
+ Request,
3
+ Response,
4
+ Agent as UndiciAgent,
5
+ fetch as undiciFetch,
6
+ } from "undici";
7
+ // @ts-ignore
8
+ import HyperDHT from "hyperdht";
9
+ // @ts-ignore
10
+ import idEncoding from "hypercore-id-encoding";
11
+
12
+ export default async function makeHyperHTTPFetch(options = {}) {
13
+ const dht = options instanceof HyperDHT ? options : new HyperDHT(options);
14
+
15
+ await dht.fullyBootstrapped();
16
+
17
+ const agent = new UndiciAgent({
18
+ connect: ({ hostname }, cb) => {
19
+ const publicKey = idEncoding.decode(hostname);
20
+
21
+ const stream = dht.connect(publicKey, {
22
+ reusableSocket: true,
23
+ });
24
+
25
+ cb(null, stream);
26
+
27
+ stream.on("error", cb);
28
+ stream.on("open", () => {
29
+ stream.removeListener("error", cb);
30
+ cb(null, stream);
31
+ });
32
+ },
33
+ });
34
+
35
+ hyperHttpFetch.close = () => dht.destroy();
36
+ return hyperHttpFetch;
37
+
38
+ /**
39
+ * @param {string|Request} resource
40
+ * @param {import("undici").RequestInit} [options]
41
+ * @returns {Promise<Response>}
42
+ */
43
+ async function hyperHttpFetch(resource, options = {}) {
44
+ const isPlainResource = typeof resource === "string";
45
+ const url = isPlainResource ? resource : resource.url;
46
+
47
+ let finalOptions = options;
48
+
49
+ if (!isPlainResource) {
50
+ finalOptions.method = resource.method;
51
+ finalOptions.headers = resource.headers;
52
+ finalOptions.body = resource.body;
53
+ }
54
+
55
+ return undiciFetch(url.replace("hyper+http:", "http:"), {
56
+ ...finalOptions,
57
+ dispatcher: agent,
58
+ });
59
+ }
60
+ }
package/src/server.js ADDED
@@ -0,0 +1,102 @@
1
+ // @ts-ignore
2
+ import HyperDHT from "hyperdht";
3
+ // @ts-ignore
4
+ import idEncoding from "hypercore-id-encoding";
5
+ import { randomBytes } from "node:crypto";
6
+ import http from "node:http";
7
+ import { Duplex, Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+
10
+ const SKIP_BODY_METHODS = new Set(["GET", "HEAD"]);
11
+
12
+ /**
13
+ * @typedef {(request: Request) => Response|Promise<Response>} HyperRequestListener
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} HyperServerOptions
18
+ * @property {HyperDHT|ConstructorParameters<typeof HyperDHT>[0]} [dht]
19
+ * @property {Buffer|Uint8Array} [seed]
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} HyperServer
24
+ * @property {string} url
25
+ * @property {{publicKey: Buffer, privateKey: Buffer}} keyPair
26
+ * @property {() => Promise<void>} destroy
27
+ */
28
+
29
+ /**
30
+ * Initialize an HTTP server for serving hyper+http requests
31
+ * @param {HyperRequestListener} onRequest
32
+ * @param {HyperServerOptions} [options]
33
+ * @returns {Promise<HyperServer>}
34
+ */
35
+ export default async function createServer(
36
+ onRequest,
37
+ { dht: dhtOptions, seed = randomBytes(32) } = {},
38
+ ) {
39
+ const keyPair = HyperDHT.keyPair(seed);
40
+
41
+ const url = `hyper+http://${idEncoding.encode(keyPair.publicKey)}/`;
42
+
43
+ // start local http server
44
+ const httpServer = http.createServer(handleRawRequest);
45
+
46
+ /** @type {import("node:http").RequestListener} */
47
+ async function handleRawRequest(req, res) {
48
+ try {
49
+ const full = new URL(req.url ?? "/", url).href;
50
+ const method = req.method ?? "GET";
51
+ const request = new Request(full, {
52
+ // @ts-ignore It's OK, just TS being weird
53
+ headers: req.headers,
54
+ method: req.method,
55
+ // @ts-ignore Exists in newer node versions
56
+ signal: req.signal,
57
+ // @ts-ignore It's OK, just TS being weird
58
+ body: SKIP_BODY_METHODS.has(method) ? null : Readable.toWeb(req),
59
+ });
60
+
61
+ const response = await onRequest(request);
62
+
63
+ for (const [name, value] of response.headers) {
64
+ res.setHeader(name, value);
65
+ }
66
+ res.writeHead(response.status);
67
+
68
+ if (response.body) {
69
+ // @ts-ignore The types match up, trust me
70
+ await pipeline(Readable.fromWeb(response.body), res);
71
+ } else {
72
+ res.end("");
73
+ }
74
+ } catch (e) {
75
+ // Expected to happen on connection close
76
+ if (e.toString().includes("ERR_STREAM_PREMATURE_CLOSE")) return;
77
+ res.writeHead(500, {
78
+ "content-type": "text/plain",
79
+ });
80
+ res.end(e.message);
81
+ }
82
+ }
83
+
84
+ // start public DHT server
85
+ const dht =
86
+ dhtOptions instanceof HyperDHT ? dhtOptions : new HyperDHT(dhtOptions);
87
+
88
+ await dht.fullyBootstrapped();
89
+
90
+ const server = dht.createServer((/** @type {Duplex} */ conn) => {
91
+ httpServer.emit("connection", conn);
92
+ });
93
+
94
+ server.listen(keyPair);
95
+
96
+ async function destroy() {
97
+ await server.close();
98
+ await dht.destroy();
99
+ }
100
+
101
+ return { url, destroy, keyPair };
102
+ }
package/test.js ADDED
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import makeHyperHTTPFetch from "./src/index.js";
3
+ import assert from "node:assert";
4
+ import createTestnet from "hyperdht/testnet.js";
5
+ import createServer from "./src/server.js";
6
+
7
+ test("Send a request to a server and get a response", async (t) => {
8
+ const testnet = await createTestnet();
9
+
10
+ t.after(() => testnet.destroy());
11
+
12
+ const url = await createTestServer(t, (_req) => new Response("Hello World"), {
13
+ dht: {
14
+ bootstrap: testnet.bootstrap,
15
+ },
16
+ });
17
+
18
+ const fetch = await makeHyperHTTPFetch({ bootstrap: testnet.bootstrap });
19
+
20
+ t.after(fetch.close);
21
+
22
+ const res = await fetch(url);
23
+ assert(res.ok, "Response is OK");
24
+ const message = await res.text();
25
+
26
+ assert.equal(message, "Hello World", "Body has expected message");
27
+ });
28
+
29
+ /**
30
+ *
31
+ * @param {import('node:test').TestContext} t
32
+ * @param {import("./src/server.js").HyperRequestListener} onRequest
33
+ * @param {import("./src/server.js").HyperServerOptions} [options]
34
+ */
35
+ async function createTestServer(t, onRequest, options = {}) {
36
+ const server = await createServer(onRequest, options);
37
+ t.after(() => server.destroy());
38
+
39
+ return server.url;
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "strict": true,
7
+ "noEmit": false,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "emitDeclarationOnly": true,
11
+ "outDir": "dist",
12
+ "checkJs": true,
13
+ "allowJs": true,
14
+ "rootDir": "src",
15
+ "skipLibCheck": true,
16
+ "resolveJsonModule": true,
17
+ "useUnknownInCatchVariables":false
18
+ },
19
+ "include": ["src/**/*.js"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }