siteward 0.0.0-stage → 0.1.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/README.md +24 -2
- package/dist/index.js +148 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# siteward
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add, list and remove Siteward monitors from the terminal. How many monitors you can have follows your plan.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g siteward
|
|
7
|
+
siteward login --url https://your-siteward.example
|
|
8
|
+
siteward add https://example.com "Example"
|
|
9
|
+
siteward list
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`login` opens your browser and signs you in through Kinde. The tool stores the access token in `~/.config/siteward/credentials.json`, readable only by you. A token lasts 24 hours, then run `siteward login` again.
|
|
13
|
+
|
|
14
|
+
| Command | What it does |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `siteward login [org_code] --url <address>` | Sign in through the browser |
|
|
17
|
+
| `siteward logout` | Delete the stored token |
|
|
18
|
+
| `siteward whoami` | Show the organization and when the sign-in ends |
|
|
19
|
+
| `siteward list` | Show monitors, their state and your plan limit |
|
|
20
|
+
| `siteward add <url> [name]` | Add a monitor. A plan at its limit answers with an error |
|
|
21
|
+
| `siteward remove <id>` | Remove a monitor |
|
|
22
|
+
|
|
23
|
+
For scripts, set `SITEWARD_URL` and `SITEWARD_TOKEN` and skip `login`.
|
|
24
|
+
|
|
25
|
+
MIT licensed.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { homedir, platform } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
const PORT = 8976;
|
|
9
|
+
const REDIRECT = `http://localhost:${PORT}/callback`;
|
|
10
|
+
const dir = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "siteward");
|
|
11
|
+
const file = join(dir, "credentials.json");
|
|
12
|
+
const b64 = (buf) => buf.toString("base64url");
|
|
13
|
+
const claims = (token) => JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
14
|
+
async function load() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function openBrowser(url) {
|
|
23
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? 'start ""' : "xdg-open";
|
|
24
|
+
exec(`${cmd} "${url}"`, () => undefined);
|
|
25
|
+
}
|
|
26
|
+
/** Signs in through the browser with the same Kinde flow as the dashboard. The tool never sees a password. */
|
|
27
|
+
async function login(args) {
|
|
28
|
+
const flag = args.indexOf("--url");
|
|
29
|
+
const url = (flag >= 0 ? args[flag + 1] : process.env.SITEWARD_URL ?? (await load())?.url ?? "").replace(/\/$/, "");
|
|
30
|
+
if (!url)
|
|
31
|
+
throw new Error("Pass the address of your Siteward: siteward login --url https://your-siteward.example");
|
|
32
|
+
const org = args.find((a, i) => a.startsWith("org_") && args[i - 1] !== "--url");
|
|
33
|
+
const cfgRes = await fetch(`${url}/cli/config`);
|
|
34
|
+
if (!cfgRes.ok)
|
|
35
|
+
throw new Error(`Could not read the sign-in settings from ${url} (${cfgRes.status}).`);
|
|
36
|
+
const { domain, clientId } = (await cfgRes.json());
|
|
37
|
+
const verifier = b64(randomBytes(32));
|
|
38
|
+
const state = b64(randomBytes(16));
|
|
39
|
+
const challenge = b64(createHash("sha256").update(verifier).digest());
|
|
40
|
+
const auth = new URL(`${domain}/oauth2/auth`);
|
|
41
|
+
auth.search = new URLSearchParams({
|
|
42
|
+
response_type: "code",
|
|
43
|
+
client_id: clientId,
|
|
44
|
+
redirect_uri: REDIRECT,
|
|
45
|
+
scope: "openid profile email",
|
|
46
|
+
code_challenge: challenge,
|
|
47
|
+
code_challenge_method: "S256",
|
|
48
|
+
state,
|
|
49
|
+
prompt: "login",
|
|
50
|
+
...(org ? { org_code: org } : {}),
|
|
51
|
+
}).toString();
|
|
52
|
+
const code = await new Promise((resolve, reject) => {
|
|
53
|
+
const server = createServer((req, res) => {
|
|
54
|
+
const u = new URL(req.url ?? "/", REDIRECT);
|
|
55
|
+
if (u.pathname !== "/callback") {
|
|
56
|
+
res.writeHead(404).end();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const ok = u.searchParams.get("state") === state && u.searchParams.get("code");
|
|
60
|
+
res.writeHead(ok ? 200 : 400, { "content-type": "text/plain" }).end(ok ? "Signed in. You can close this tab." : "Sign-in failed. Run siteward login again.");
|
|
61
|
+
server.close();
|
|
62
|
+
ok ? resolve(u.searchParams.get("code")) : reject(new Error(u.searchParams.get("error_description") ?? "The sign-in could not be checked."));
|
|
63
|
+
});
|
|
64
|
+
server.on("error", (e) => reject(new Error(`Could not listen on port ${PORT}: ${e.message}`)));
|
|
65
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
66
|
+
console.log("Opening your browser to sign in. If it does not open, go to:\n" + auth.toString());
|
|
67
|
+
openBrowser(auth.toString());
|
|
68
|
+
});
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
server.close();
|
|
71
|
+
reject(new Error("Timed out after 5 minutes."));
|
|
72
|
+
}, 300_000).unref();
|
|
73
|
+
});
|
|
74
|
+
const tokenRes = await fetch(`${domain}/oauth2/token`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
77
|
+
body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, code, code_verifier: verifier, redirect_uri: REDIRECT }),
|
|
78
|
+
});
|
|
79
|
+
if (!tokenRes.ok)
|
|
80
|
+
throw new Error("Kinde did not accept the sign-in.");
|
|
81
|
+
const body = (await tokenRes.json());
|
|
82
|
+
const exp = claims(body.access_token).exp ?? 0;
|
|
83
|
+
await mkdir(dir, { recursive: true });
|
|
84
|
+
await writeFile(file, JSON.stringify({ url, token: body.access_token, expires_at: exp * 1000 }), { mode: 0o600 });
|
|
85
|
+
console.log(`Signed in to ${claims(body.access_token).org_code ?? "your organization"}.`);
|
|
86
|
+
}
|
|
87
|
+
async function session() {
|
|
88
|
+
const saved = await load();
|
|
89
|
+
const url = (process.env.SITEWARD_URL ?? saved?.url ?? "").replace(/\/$/, "");
|
|
90
|
+
const token = process.env.SITEWARD_TOKEN ?? saved?.token ?? "";
|
|
91
|
+
if (!url || !token)
|
|
92
|
+
throw new Error("You are not signed in. Run: siteward login --url https://your-siteward.example");
|
|
93
|
+
if (!process.env.SITEWARD_TOKEN && saved && saved.expires_at < Date.now())
|
|
94
|
+
throw new Error("Your sign-in has expired. Run: siteward login");
|
|
95
|
+
return { url, token };
|
|
96
|
+
}
|
|
97
|
+
async function call(method, path, body) {
|
|
98
|
+
const { url, token } = await session();
|
|
99
|
+
const res = await fetch(`${url}${path}`, {
|
|
100
|
+
method,
|
|
101
|
+
headers: { authorization: `Bearer ${token}`, ...(body ? { "content-type": "application/json" } : {}) },
|
|
102
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
103
|
+
});
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
const data = text ? JSON.parse(text) : null;
|
|
106
|
+
if (res.status === 401)
|
|
107
|
+
throw new Error("Your sign-in is not valid any more. Run: siteward login");
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(data?.error ?? `request failed: ${res.status}`);
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
const [command, ...args] = process.argv.slice(2);
|
|
113
|
+
try {
|
|
114
|
+
if (command === "login") {
|
|
115
|
+
await login(args);
|
|
116
|
+
}
|
|
117
|
+
else if (command === "logout") {
|
|
118
|
+
await rm(file, { force: true });
|
|
119
|
+
console.log("Signed out.");
|
|
120
|
+
}
|
|
121
|
+
else if (command === "whoami") {
|
|
122
|
+
const { token, url } = await session();
|
|
123
|
+
const c = claims(token);
|
|
124
|
+
console.log(`${c.org_code ?? "no organization"} at ${url}, signed in until ${new Date((c.exp ?? 0) * 1000).toISOString()}`);
|
|
125
|
+
}
|
|
126
|
+
else if (command === "list") {
|
|
127
|
+
const { limits, monitors } = await call("GET", "/api/monitors");
|
|
128
|
+
console.log(`${monitors.length} of ${limits?.monitors ?? "unlimited"} monitors`);
|
|
129
|
+
for (const m of monitors)
|
|
130
|
+
console.log(`${m.status.padEnd(12)} ${m.name.padEnd(24)} ${m.url} ${m.id}`);
|
|
131
|
+
}
|
|
132
|
+
else if (command === "add" && args[0]) {
|
|
133
|
+
const m = await call("POST", "/api/monitors", { url: args[0], name: args[1] });
|
|
134
|
+
console.log(`added ${m.name}`);
|
|
135
|
+
}
|
|
136
|
+
else if (command === "remove" && args[0]) {
|
|
137
|
+
await call("DELETE", `/api/monitors/${args[0]}`);
|
|
138
|
+
console.log("removed");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log("Usage: siteward login [org_code] --url <address> | logout | whoami | list | add <url> [name] | remove <id>");
|
|
142
|
+
process.exitCode = command ? 1 : 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(error instanceof Error ? error.message : error);
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "siteward",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add, list and remove Siteward monitors from the terminal",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"siteward": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"typescript": "^5.6.0"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/sholajegede/siteward.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"uptime",
|
|
30
|
+
"monitoring",
|
|
31
|
+
"cli",
|
|
32
|
+
"kinde",
|
|
33
|
+
"cloudflare"
|
|
34
|
+
],
|
|
35
|
+
"homepage": "https://github.com/sholajegede/siteward#readme",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|