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 +21 -0
- package/README.md +82 -0
- package/eslint.config.js +6 -0
- package/package.json +51 -0
- package/src/index.js +60 -0
- package/src/server.js +102 -0
- package/test.js +40 -0
- package/tsconfig.json +21 -0
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
|
+
```
|
package/eslint.config.js
ADDED
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
|
+
}
|