siteward 0.0.0-stage → 0.1.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 (3) hide show
  1. package/README.md +71 -2
  2. package/dist/index.js +148 -0
  3. package/package.json +37 -4
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # siteward
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Add, list and remove [Siteward](https://github.com/sholajegede/siteward) monitors from the terminal. How many monitors you can have follows your plan.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g siteward
9
+ ```
10
+
11
+ You need Node.js 20 or later. To run it once without installing, use `npx siteward`.
12
+
13
+ ## Start
14
+
15
+ This tool talks to a Siteward server. You deploy that server yourself, from the [repository](https://github.com/sholajegede/siteward), or use the address of one that you have an account on.
16
+
17
+ ```bash
18
+ siteward login --url https://<your-siteward-server>
19
+ siteward add https://example.com "Example"
20
+ siteward list
21
+ ```
22
+
23
+ `login` opens your browser and signs you in through Kinde. The tool never sees your password.
24
+
25
+ ## Commands
26
+
27
+ | Command | What it does |
28
+ | --- | --- |
29
+ | `siteward login [org_code] --url <address>` | Sign in through the browser. Pass an organization code to choose one. |
30
+ | `siteward logout` | Delete the stored token. |
31
+ | `siteward whoami` | Show the organization, the server and when the sign-in ends. |
32
+ | `siteward list` | Show monitors, their state and your plan limit. |
33
+ | `siteward add <url> [name]` | Add a monitor. A plan at its limit answers with an error. |
34
+ | `siteward remove <id>` | Remove a monitor. |
35
+
36
+ The address of the server is saved after `login`, so later commands need no `--url`.
37
+
38
+ ## Monitor states
39
+
40
+ | State | Meaning |
41
+ | --- | --- |
42
+ | `active` | The monitor runs. |
43
+ | `paused_plan` | Your plan allows fewer monitors than you have. The newest monitors pause first, and they resume when the limit rises. |
44
+ | `paused_user` | You paused it. A plan change does not resume it. |
45
+
46
+ ## Sign-in and storage
47
+
48
+ - The tool signs in with OAuth 2.0 and PKCE. It listens on `http://localhost:8976/callback` for the answer, so that address must be an allowed callback URL in your Kinde application.
49
+ - The access token is saved in `~/.config/siteward/credentials.json` (or under `$XDG_CONFIG_HOME`), readable only by you.
50
+ - A token lasts 24 hours. After that, run `siteward login` again. `siteward logout` deletes the file at once.
51
+
52
+ ## Use it in scripts
53
+
54
+ Set two variables and skip `login`:
55
+
56
+ ```bash
57
+ export SITEWARD_URL=https://<your-siteward-server>
58
+ export SITEWARD_TOKEN=<Kinde access token>
59
+ siteward list
60
+ ```
61
+
62
+ ## Errors
63
+
64
+ | Message | What to do |
65
+ | --- | --- |
66
+ | `Your plan allows N monitors.` | Remove a monitor or upgrade the plan. |
67
+ | `Sign in again.` | The token expired. Run `siteward login`. |
68
+ | `Pass the address of your Siteward` | Add `--url https://<your-siteward-server>` to `login`. |
69
+
70
+ ## License
71
+
72
+ MIT
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
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
+ }