@decentnetwork/beagle-service 0.0.0-stage → 0.2.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 +720 -2
- package/dist/agent.d.ts +18 -0
- package/dist/agent.js +448 -0
- package/dist/auth.d.ts +28 -0
- package/dist/auth.js +340 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +163 -0
- package/dist/commonjs.cjs +49 -0
- package/dist/commonjs.d.cts +11 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +191 -0
- package/dist/outbox.d.ts +63 -0
- package/dist/outbox.js +314 -0
- package/dist/routes.d.ts +29 -0
- package/dist/routes.js +454 -0
- package/dist/stores.d.ts +21 -0
- package/dist/stores.js +92 -0
- package/dist/types.d.ts +641 -0
- package/dist/types.js +1 -0
- package/dist/users.d.ts +16 -0
- package/dist/users.js +429 -0
- package/dist/util.d.ts +48 -0
- package/dist/util.js +208 -0
- package/examples/minimal-site/index.html +91 -0
- package/examples/minimal-site/server.mjs +64 -0
- package/examples/next-pages-cjs/README.md +30 -0
- package/examples/next-pages-cjs/instrumentation.js +16 -0
- package/examples/next-pages-cjs/lib/beagle.js +23 -0
- package/examples/next-pages-cjs/lib/peer-for-browser.mjs +2 -0
- package/examples/next-pages-cjs/next.config.js +11 -0
- package/examples/next-pages-cjs/package.json +19 -0
- package/examples/next-pages-cjs/pages/api/auth/[...nextauth].js +17 -0
- package/examples/next-pages-cjs/pages/api/auth/launch.js +3 -0
- package/examples/next-pages-cjs/pages/api/auth/logout.js +3 -0
- package/examples/next-pages-cjs/pages/api/auth/me.js +3 -0
- package/examples/next-pages-cjs/pages/api/auth/nonce.js +3 -0
- package/examples/next-pages-cjs/pages/api/auth/register.js +3 -0
- package/examples/next-pages-cjs/pages/api/auth/sign-in.js +3 -0
- package/examples/next-pages-cjs/pages/api/notes.js +16 -0
- package/examples/next-pages-cjs/pages/index.js +52 -0
- package/examples/sign-in-from-script.mjs +37 -0
- package/package.json +67 -4
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>Minimal site</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font: 16px/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
|
|
8
|
+
button, input { font: inherit; padding: .5rem .8rem; margin: .2rem .2rem .2rem 0; }
|
|
9
|
+
code { word-break: break-all; }
|
|
10
|
+
#error { color: #b00020; }
|
|
11
|
+
</style>
|
|
12
|
+
<h1>Minimal site</h1>
|
|
13
|
+
<div id="out">Loading…</div>
|
|
14
|
+
<p id="error"></p>
|
|
15
|
+
|
|
16
|
+
<script type="module">
|
|
17
|
+
import { signIn, readLaunch, addFriend } from "/beagle-connect.js";
|
|
18
|
+
|
|
19
|
+
// Two doors, one origin each: an https page cannot probe for an installed
|
|
20
|
+
// Beagle, so the visitor says which one they use. A Beagle on another local
|
|
21
|
+
// port: open this page with ?beagle=http://127.0.0.1:8767
|
|
22
|
+
const local = new URLSearchParams(location.search).get("beagle");
|
|
23
|
+
const DOORS = {
|
|
24
|
+
installed: /^http:\/\/(127\.0\.0\.1|localhost):\d+$/.test(local ?? "") ? local : "http://127.0.0.1:8766",
|
|
25
|
+
web: "https://app.beagle.chat",
|
|
26
|
+
};
|
|
27
|
+
const out = document.querySelector("#out");
|
|
28
|
+
const fail = (e) => (document.querySelector("#error").textContent = e?.message ?? String(e));
|
|
29
|
+
const api = async (path, body) => {
|
|
30
|
+
const r = await fetch(`/api/auth/${path}`, body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {});
|
|
31
|
+
return { status: r.status, ...(await r.json()) };
|
|
32
|
+
};
|
|
33
|
+
const el = (tag, props = {}, ...kids) => {
|
|
34
|
+
const n = Object.assign(document.createElement(tag), props);
|
|
35
|
+
n.append(...kids);
|
|
36
|
+
return n;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async function signInThrough(door) {
|
|
40
|
+
// Open the window inside the click: one opened after an await has lost the user gesture.
|
|
41
|
+
const win = window.open("about:blank", "beagle-connect", "width=420,height=560");
|
|
42
|
+
if (!win) return fail(new Error("Allow pop-ups for this site, then click again."));
|
|
43
|
+
try {
|
|
44
|
+
const { nonce } = await api("nonce");
|
|
45
|
+
const who = await signIn({ nonce, origins: [DOORS[door]], existingWindow: win });
|
|
46
|
+
render(await api("sign-in", who)); // { userid, address, name, avatar, sig, nonce, uiOrigin }
|
|
47
|
+
} catch (e) {
|
|
48
|
+
try { win.close(); } catch {}
|
|
49
|
+
fail(e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function render(me) {
|
|
54
|
+
document.querySelector("#error").textContent = "";
|
|
55
|
+
out.replaceChildren();
|
|
56
|
+
if (!me.user) {
|
|
57
|
+
if (me.error && me.error !== "not-signed-in") fail(new Error(me.message));
|
|
58
|
+
out.append(
|
|
59
|
+
el("button", { textContent: "Sign in with Beagle (installed)", onclick: () => signInThrough("installed") }),
|
|
60
|
+
el("button", { textContent: "Sign in with Beagle in the browser", onclick: () => signInThrough("web") }),
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { user, registration, agent } = me;
|
|
65
|
+
out.append(el("p", {}, `Signed in as ${user.name ?? "someone"} (${user.status}) `, el("code", { textContent: user.userid })));
|
|
66
|
+
if (registration.missing.length || registration.optional.length) {
|
|
67
|
+
const form = el("form", {}, el("p", { textContent: registration.due ? "Please register to go on:" : registration.missing.length ? "Register:" : "Your details:" }));
|
|
68
|
+
for (const f of [...registration.required, ...registration.optional]) {
|
|
69
|
+
form.append(el("input", { name: f, placeholder: f + (registration.required.includes(f) ? " (required)" : ""), value: user.profile[f] ?? "" }));
|
|
70
|
+
}
|
|
71
|
+
form.append(el("button", { textContent: "Save" }));
|
|
72
|
+
form.onsubmit = async (ev) => {
|
|
73
|
+
ev.preventDefault();
|
|
74
|
+
const r = await api("register", { profile: Object.fromEntries(new FormData(form)) });
|
|
75
|
+
r.ok ? render(r) : fail(new Error(Object.entries(r.fields ?? {}).map(([k, v]) => `${k}: ${v}`).join(", ") || r.message));
|
|
76
|
+
};
|
|
77
|
+
out.append(form);
|
|
78
|
+
}
|
|
79
|
+
if (agent) out.append(el("button", { textContent: `Add ${agent.name ?? "our agent"} on Beagle`, onclick: () => addFriend({ address: agent.address, name: agent.name }).catch(fail) }));
|
|
80
|
+
out.append(el("button", { textContent: "Sign out", onclick: async () => { await api("logout", {}); render({}); } }));
|
|
81
|
+
fetch("/api/hello").then((r) => r.json()).then((j) => out.append(el("p", { textContent: j.message ?? `The site's own route says: ${j.error}` })));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Arrived from Beagle's Apps tab? Then the visitor is signed in already.
|
|
85
|
+
try {
|
|
86
|
+
const launch = await readLaunch();
|
|
87
|
+
if (launch) await api("launch", launch);
|
|
88
|
+
} catch (e) { fail(e); }
|
|
89
|
+
render(await api("me"));
|
|
90
|
+
</script>
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A whole site on Beagle identity in one file.
|
|
2
|
+
// npm install @decentnetwork/beagle-service @decentnetwork/beagle-connect esbuild
|
|
3
|
+
// node server.mjs → http://localhost:3000
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { build } from "esbuild";
|
|
8
|
+
import { createBeagleService } from "@decentnetwork/beagle-service";
|
|
9
|
+
|
|
10
|
+
const PORT = Number(process.env.PORT ?? 3000);
|
|
11
|
+
const ORIGIN = process.env.ORIGIN ?? `http://localhost:${PORT}`; // exactly what the browser's address bar shows
|
|
12
|
+
|
|
13
|
+
const svc = await createBeagleService({
|
|
14
|
+
origins: [ORIGIN],
|
|
15
|
+
dataDir: process.env.DATA_DIR ?? fileURLToPath(new URL("./data", import.meta.url)),
|
|
16
|
+
register: { required: ["email"], optional: ["location"] }, // [] = every visitor is a user at once
|
|
17
|
+
visitor: { maxDays: 14, then: "ask" }, // "refuse" = 403 until they register
|
|
18
|
+
// The site's own Carrier identity. Leave it out if the site only needs sign-in.
|
|
19
|
+
agent: process.env.AGENT ? { nickname: "Minimal Site", accept: "signed-in", befriendOn: "register" } : undefined,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
svc.agent?.onMessage(({ user, text, reply }) => reply(user ? `Hello ${user.name ?? "there"}, you said: ${text}` : "Sign in on the site first."));
|
|
23
|
+
svc.users.on("registered", (user) => console.log(`registered: ${user.userid} ${JSON.stringify(user.profile)}`));
|
|
24
|
+
|
|
25
|
+
// beagle-connect for the browser. It needs two functions of @decentnetwork/peer
|
|
26
|
+
// (the signature check), so bundle those two modules instead of the whole peer.
|
|
27
|
+
const peerForBrowser = {
|
|
28
|
+
name: "peer-for-browser",
|
|
29
|
+
setup(b) {
|
|
30
|
+
b.onResolve({ filter: /^@decentnetwork\/peer$/ }, () => ({ path: "peer", namespace: "shim" }));
|
|
31
|
+
b.onLoad({ filter: /.*/, namespace: "shim" }, () => ({
|
|
32
|
+
resolveDir: fileURLToPath(new URL(".", import.meta.url)),
|
|
33
|
+
contents: `export { verifyDetached } from "@decentnetwork/peer/dist/crypto/sign.js";
|
|
34
|
+
export { base58ToBytes } from "@decentnetwork/peer/dist/utils/base58.js";`,
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const client = (await build({
|
|
39
|
+
entryPoints: [fileURLToPath(import.meta.resolve("@decentnetwork/beagle-connect"))],
|
|
40
|
+
bundle: true, format: "esm", platform: "browser", write: false, plugins: [peerForBrowser],
|
|
41
|
+
})).outputFiles[0].text;
|
|
42
|
+
|
|
43
|
+
const page = readFileSync(new URL("./index.html", import.meta.url));
|
|
44
|
+
const auth = svc.nodeHandler({ prefix: "/api/auth" }); // nonce, sign-in, launch, me, logout, register
|
|
45
|
+
const send = (res, status, type, body) => { res.writeHead(status, { "content-type": type, "cache-control": "no-store" }); res.end(body); };
|
|
46
|
+
|
|
47
|
+
http.createServer(async (req, res) => {
|
|
48
|
+
const path = new URL(req.url, ORIGIN).pathname;
|
|
49
|
+
if (path.startsWith("/api/auth/")) return auth(req, res);
|
|
50
|
+
if (path === "/api/hello") {
|
|
51
|
+
// One of the site's own routes: who is asking?
|
|
52
|
+
const who = await svc.auth.authenticate(req);
|
|
53
|
+
if (!who.ok) return send(res, who.status, "application/json", JSON.stringify({ error: who.error }));
|
|
54
|
+
return send(res, 200, "application/json", JSON.stringify({ message: `Hello ${who.user.name ?? who.user.userid}, you are ${who.user.status}.` }));
|
|
55
|
+
}
|
|
56
|
+
if (path === "/beagle-connect.js") return send(res, 200, "text/javascript; charset=utf-8", client);
|
|
57
|
+
if (path === "/") return send(res, 200, "text/html; charset=utf-8", page);
|
|
58
|
+
send(res, 404, "text/plain", "not found");
|
|
59
|
+
}).listen(PORT, () => {
|
|
60
|
+
console.log(`minimal-site: ${ORIGIN}`);
|
|
61
|
+
if (svc.agent) console.log(`agent address (add it in Beagle): ${svc.agent.address}`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// To refuse someone: await svc.users.block(userid, "spam") (sign-in answers 403, sessions end, the agent drops them)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Sign in with Beagle beside NextAuth, in a CommonJS Next.js site
|
|
2
|
+
|
|
3
|
+
A pages-router site whose `lib/` is CommonJS and which already has NextAuth's catch-all at
|
|
4
|
+
`pages/api/auth/[...nextauth].js`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install
|
|
8
|
+
npm run build
|
|
9
|
+
BEAGLE_ORIGINS=http://localhost:3000 npm start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| File | What it shows |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `lib/beagle.js` | `require("@decentnetwork/beagle-service")`, one service per process, the `onSignIn` hook |
|
|
15
|
+
| `instrumentation.js` | the service is created when the server starts, so a wrong `BEAGLE_ORIGINS` stops the start |
|
|
16
|
+
| `pages/api/auth/[...nextauth].js` | NextAuth's catch-all, untouched |
|
|
17
|
+
| `pages/api/auth/nonce.js`, `sign-in.js`, `launch.js`, `me.js`, `logout.js`, `register.js` | one Beagle route per file: `export default beagle.route("signIn")` |
|
|
18
|
+
| `pages/api/notes.js` | a route of the site's own: `beagle.authenticate(req, { checkOrigin: true })` |
|
|
19
|
+
| `pages/index.js` | reading the session in `getServerSideProps`; the popup with `beagle-connect` |
|
|
20
|
+
| `next.config.js`, `lib/peer-for-browser.mjs` | what the browser bundle needs for `beagle-connect` |
|
|
21
|
+
|
|
22
|
+
The operator's command works beside the running server:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx beagle-service users list
|
|
26
|
+
npx beagle-service users block <userid> spam
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`npm run e2e:next` in the package builds this example against the package as it would be published and runs it
|
|
30
|
+
with `next build` and `next start`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Next.js runs this once when the server starts (`next start`, `next dev`),
|
|
2
|
+
// before any request. Creating the service here means a wrong configuration
|
|
3
|
+
// (an entry of BEAGLE_ORIGINS that is not an exact origin, a users file that
|
|
4
|
+
// does not parse) is found at start, with its reason, instead of showing up as
|
|
5
|
+
// a failed sign-in later.
|
|
6
|
+
//
|
|
7
|
+
// When `register` rejects, Next.js prints "Failed to prepare server" with the
|
|
8
|
+
// reason and answers 500 to every request until the configuration is fixed and
|
|
9
|
+
// the server started again. To keep the rest of the site up and have only the
|
|
10
|
+
// sign-in routes answer 500, do not await: `beagle.ready().catch(() => {})`
|
|
11
|
+
// (the reason is still printed at start).
|
|
12
|
+
export async function register() {
|
|
13
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
14
|
+
const { beagle } = require("./lib/beagle");
|
|
15
|
+
await beagle.ready();
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The site's one beagle-service. CommonJS, like the rest of this lib/.
|
|
2
|
+
//
|
|
3
|
+
// `sharedBeagleService` returns at once and creates the service on first use,
|
|
4
|
+
// one per process however many times Next.js evaluates this file. It never
|
|
5
|
+
// throws at a request: while the service cannot start, the routes answer 500
|
|
6
|
+
// and `authenticate` answers `ok: false`.
|
|
7
|
+
const { sharedBeagleService } = require("@decentnetwork/beagle-service");
|
|
8
|
+
|
|
9
|
+
const list = (v) => String(v || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10
|
+
|
|
11
|
+
const beagle = sharedBeagleService(() => ({
|
|
12
|
+
// Exact origins of this site's pages: scheme, host and port.
|
|
13
|
+
origins: list(process.env.BEAGLE_ORIGINS || `http://localhost:${process.env.PORT || 3000}`),
|
|
14
|
+
dataDir: process.env.BEAGLE_DATA_DIR || "./data",
|
|
15
|
+
// The site's part of a sign-in, awaited before the answer goes out. What it
|
|
16
|
+
// returns is added to the answer; `{ refuse: "why" }` refuses the sign-in.
|
|
17
|
+
onSignIn: async (user, { origin, door, first }) => {
|
|
18
|
+
if (first) console.log(`first sign-in: ${user.userid} at ${origin} through ${door}`);
|
|
19
|
+
return { role: user.signIns > 100 ? "regular" : "listener" };
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
module.exports = { beagle };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
webpack(config, { isServer }) {
|
|
5
|
+
// The page uses beagle-connect, which needs two functions of
|
|
6
|
+
// @decentnetwork/peer (the signature check). Give the browser those two
|
|
7
|
+
// modules instead of the whole peer.
|
|
8
|
+
if (!isServer) config.resolve.alias["@decentnetwork/peer$"] = path.join(__dirname, "lib/peer-for-browser.mjs");
|
|
9
|
+
return config;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "beagle-service-next-pages-cjs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "Sign in with Beagle in a CommonJS Next.js pages-router site that also has NextAuth's catch-all.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"users": "beagle-service users list"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@decentnetwork/beagle-connect": "^0.1.2",
|
|
13
|
+
"@decentnetwork/beagle-service": "^0.2.0",
|
|
14
|
+
"next": "^15.3.2",
|
|
15
|
+
"next-auth": "^4.24.14",
|
|
16
|
+
"react": "^19.1.0",
|
|
17
|
+
"react-dom": "^19.1.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// NextAuth owns the catch-all under /api/auth. A second catch-all in this
|
|
2
|
+
// directory does not build, so the Beagle routes beside it are one file each
|
|
3
|
+
// (nonce.js, sign-in.js, launch.js, me.js, logout.js, register.js); a named
|
|
4
|
+
// file wins over the catch-all, and none of those names is one of NextAuth's.
|
|
5
|
+
import NextAuth from "next-auth";
|
|
6
|
+
import Credentials from "next-auth/providers/credentials";
|
|
7
|
+
|
|
8
|
+
export default NextAuth({
|
|
9
|
+
secret: process.env.NEXTAUTH_SECRET || "example-only-secret-change-me",
|
|
10
|
+
providers: [
|
|
11
|
+
Credentials({
|
|
12
|
+
name: "Demo",
|
|
13
|
+
credentials: { name: { label: "Name", type: "text" } },
|
|
14
|
+
authorize: async (credentials) => (credentials?.name ? { id: credentials.name, name: credentials.name } : null),
|
|
15
|
+
}),
|
|
16
|
+
],
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// One of the site's own routes. Reading a session is asynchronous and never
|
|
2
|
+
// throws: no session, a bad one, a blocked user or a server that cannot check
|
|
3
|
+
// all come back as `ok: false` with a status.
|
|
4
|
+
const { beagle } = require("../../lib/beagle");
|
|
5
|
+
|
|
6
|
+
const notes = new Map(); // userid -> text; a real site has a database
|
|
7
|
+
|
|
8
|
+
export default async function handler(req, res) {
|
|
9
|
+
// checkOrigin: a request that changes something and carries the session
|
|
10
|
+
// cookie must come from one of this site's pages (its Origin header is on
|
|
11
|
+
// the list). A GET is not checked.
|
|
12
|
+
const who = await beagle.authenticate(req, { checkOrigin: true });
|
|
13
|
+
if (!who.ok) return res.status(who.status).json({ error: who.error });
|
|
14
|
+
if (req.method === "POST") notes.set(who.user.userid, String(req.body?.text ?? "").slice(0, 500));
|
|
15
|
+
res.status(200).json({ note: notes.get(who.user.userid) ?? "" });
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { beagle } from "../lib/beagle"; // used on the server only, in getServerSideProps
|
|
3
|
+
|
|
4
|
+
export async function getServerSideProps({ req }) {
|
|
5
|
+
const who = await beagle.authenticate(req);
|
|
6
|
+
return { props: { user: who.ok ? { userid: who.user.userid, name: who.user.name } : null } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Home({ user }) {
|
|
10
|
+
const [me, setMe] = useState(user);
|
|
11
|
+
const [error, setError] = useState("");
|
|
12
|
+
|
|
13
|
+
async function signInWithBeagle() {
|
|
14
|
+
// Open the window inside the click, or the browser blocks it.
|
|
15
|
+
const win = window.open("about:blank", "beagle-connect", "width=420,height=560");
|
|
16
|
+
try {
|
|
17
|
+
const { signIn } = await import("@decentnetwork/beagle-connect");
|
|
18
|
+
const { nonce } = await (await fetch("/api/auth/nonce")).json();
|
|
19
|
+
const who = await signIn({ nonce, existingWindow: win });
|
|
20
|
+
const res = await fetch("/api/auth/sign-in", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(who) });
|
|
21
|
+
const answer = await res.json();
|
|
22
|
+
if (!res.ok) throw new Error(answer.message || answer.error);
|
|
23
|
+
setMe(answer.user);
|
|
24
|
+
setError("");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
win?.close();
|
|
27
|
+
setError(String(e.message || e));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function signOut() {
|
|
32
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
33
|
+
setMe(null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<main style={{ font: "16px system-ui", maxWidth: 560, margin: "64px auto", padding: "0 16px" }}>
|
|
38
|
+
<h1>Sign in with Beagle</h1>
|
|
39
|
+
{me ? (
|
|
40
|
+
<>
|
|
41
|
+
<p>Signed in as <b>{me.name || "someone"}</b></p>
|
|
42
|
+
<p><code style={{ wordBreak: "break-all" }}>{me.userid}</code></p>
|
|
43
|
+
<button onClick={signOut}>Sign out</button>
|
|
44
|
+
</>
|
|
45
|
+
) : (
|
|
46
|
+
<button onClick={signInWithBeagle}>Sign in with Beagle</button>
|
|
47
|
+
)}
|
|
48
|
+
{error && <p role="alert">{error}</p>}
|
|
49
|
+
<p><a href="/api/auth/signin">NextAuth's own sign-in page</a> lives beside it.</p>
|
|
50
|
+
</main>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A script that signs in to a site: a publisher node, a cron job, a test.
|
|
2
|
+
//
|
|
3
|
+
// node sign-in-from-script.mjs https://your.app
|
|
4
|
+
//
|
|
5
|
+
// A browser adds the `Origin` header by itself; a script has to. The server
|
|
6
|
+
// takes the origin from that header only (never from the body), checks it
|
|
7
|
+
// against its exact list, and checks that the signature was made for it. So a
|
|
8
|
+
// script names the site's own origin, the one its pages are served from, in
|
|
9
|
+
// the header and in the string it signs.
|
|
10
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
11
|
+
import { bytesToBase58, signDetached } from "@decentnetwork/peer";
|
|
12
|
+
|
|
13
|
+
const site = new URL(process.argv[2] ?? "http://localhost:3000").origin;
|
|
14
|
+
const sendOrigin = !process.argv.includes("--no-origin");
|
|
15
|
+
|
|
16
|
+
// The script's identity. A real one loads its key from a file it keeps; this
|
|
17
|
+
// one makes a throwaway X25519 key so the example runs anywhere.
|
|
18
|
+
const { publicKey, privateKey } = generateKeyPairSync("x25519");
|
|
19
|
+
const secretKey = new Uint8Array(Buffer.from(privateKey.export({ format: "jwk" }).d, "base64url"));
|
|
20
|
+
const userid = bytesToBase58(new Uint8Array(Buffer.from(publicKey.export({ format: "jwk" }).x, "base64url")));
|
|
21
|
+
const headers = sendOrigin ? { Origin: site, "content-type": "application/json" } : { "content-type": "application/json" };
|
|
22
|
+
|
|
23
|
+
// README:begin
|
|
24
|
+
const { nonce } = await (await fetch(`${site}/api/auth/nonce`)).json();
|
|
25
|
+
const sig = Buffer.from(signDetached(secretKey, new TextEncoder().encode(`decent-auth\n${site}\n${nonce}`))).toString("hex");
|
|
26
|
+
const res = await fetch(`${site}/api/auth/sign-in`, { method: "POST", headers, body: JSON.stringify({ userid, nonce, sig }) });
|
|
27
|
+
// README:end
|
|
28
|
+
|
|
29
|
+
const answer = await res.json();
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
console.error(`sign-in refused: ${res.status} ${answer.error}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// The session: send this cookie back on later requests (or use `session: { transport: "bearer" }` on the site).
|
|
35
|
+
const cookie = res.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
|
|
36
|
+
const me = await (await fetch(`${site}/api/auth/me`, { headers: { cookie } })).json();
|
|
37
|
+
console.log(`signed in as ${me.user.userid}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle-service",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The web backend for a site on Beagle identity: Sign in with Beagle on the server, a user store with registration and a blacklist, and the site's own Carrier agent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/commonjs.cjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/commonjs.d.cts",
|
|
17
|
+
"default": "./dist/commonjs.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"beagle-service": "./dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"examples",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "npm run build && tsc -p test/types/tsconfig.json && node --test --test-force-exit test/*.test.mjs",
|
|
35
|
+
"e2e": "npm run build && node test/e2e/agent-public.mjs",
|
|
36
|
+
"prepack": "npm run build",
|
|
37
|
+
"e2e:next": "node test/e2e/next-example.mjs",
|
|
38
|
+
"test:gaps-on-0.1.0": "node test/gaps-on-0.1.0.mjs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@decentnetwork/beagle-connect": "^0.1.2",
|
|
42
|
+
"@decentnetwork/peer": "^0.1.178"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.11.0",
|
|
46
|
+
"typescript": "^5.3.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"beagle",
|
|
53
|
+
"carrier",
|
|
54
|
+
"decent network",
|
|
55
|
+
"sign-in",
|
|
56
|
+
"authentication",
|
|
57
|
+
"p2p",
|
|
58
|
+
"agent"
|
|
59
|
+
],
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/0xli/beagle.git",
|
|
66
|
+
"directory": "packages/beagle-service"
|
|
67
|
+
},
|
|
68
|
+
"module": "./dist/index.js"
|
|
69
|
+
}
|