toiljs 0.0.84 → 0.0.86
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/CHANGELOG.md +10 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/generate.js +3 -2
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +44 -5
- package/build/compiler/toil-docs.generated.js +6 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +113 -33
- package/build/devserver/runtime/module.js +1 -0
- package/docs/auth/configuration.md +94 -0
- package/docs/auth/extending.md +170 -0
- package/docs/auth/how-it-works.md +138 -0
- package/docs/auth/index.md +102 -0
- package/docs/auth/usage.md +188 -0
- package/docs/auth.md +6 -0
- package/examples/basic/client/routes/analytics.tsx +164 -26
- package/examples/basic/server/models/SiteAnalytics.ts +139 -11
- package/examples/basic/server/routes/Analytics.ts +89 -13
- package/examples/basic/server/routes/UserId.ts +22 -0
- package/package.json +5 -1
- package/scripts/gen-toil-docs.mjs +15 -6
- package/server/auth/AuthController.ts +335 -0
- package/server/auth/AuthUser.ts +49 -0
- package/server/auth/index.ts +16 -0
- package/server/globals/auth.ts +31 -0
- package/server/globals/userid.ts +107 -0
- package/src/compiler/config.ts +13 -0
- package/src/compiler/generate.ts +3 -2
- package/src/compiler/index.ts +74 -5
- package/src/compiler/toil-docs.generated.ts +6 -1
- package/src/devserver/analytics/index.ts +139 -38
- package/src/devserver/runtime/module.ts +1 -0
- package/test/analytics-dev.test.ts +76 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Using auth
|
|
2
|
+
|
|
3
|
+
## 1. Enable it
|
|
4
|
+
|
|
5
|
+
Either the config flag (canonical) or a one-line import — both do the same thing (the build appends the
|
|
6
|
+
framework's auth controller + user shape to your server as compiled entries, so `/auth/*` self-mounts):
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// toil.config.ts — canonical
|
|
10
|
+
export default defineConfig({ server: { auth: true } });
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// server/main.ts — escape hatch (equivalent)
|
|
15
|
+
import 'toiljs/server/auth';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
There is also an `AuthService.enable()` no-op you can call for discoverability, but enabling is done by the
|
|
19
|
+
flag/import above (it happens at build time). Turn it off by removing the flag/import — with neither, no
|
|
20
|
+
`/auth` routes and no accounts collection are generated. It is strictly opt-in.
|
|
21
|
+
|
|
22
|
+
## 2. The client
|
|
23
|
+
|
|
24
|
+
`toiljs/client` ships the browser half — it runs the OPRF blinding, Argon2id, ML-DSA keygen, and ML-KEM
|
|
25
|
+
encapsulation, and talks to `/auth/*`. You never touch the crypto.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Auth } from 'toiljs/client';
|
|
29
|
+
|
|
30
|
+
// Create an account. Throws on a taken username or a transport error.
|
|
31
|
+
await Auth.register(username, password);
|
|
32
|
+
|
|
33
|
+
// Log in. On success the browser holds the signed session cookie; subsequent
|
|
34
|
+
// requests to @auth routes are authorized automatically.
|
|
35
|
+
await Auth.login(username, password);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Options (second argument, `AuthOptions`):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
await Auth.login(username, password, {
|
|
42
|
+
baseUrl: '/auth', // default; change if you mount elsewhere
|
|
43
|
+
serverKemPublicKey: MY_KEM_PUB, // REQUIRED in production — pin your deployment's KEM key
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Production:** the client ships with the DEV KEM public key pinned. A real deployment MUST pass its own
|
|
48
|
+
> `serverKemPublicKey` (derived from `AUTH_KEM_SK`). See [Configuration](./configuration.md).
|
|
49
|
+
|
|
50
|
+
## 2b. Client-side: who's logged in, and protecting pages
|
|
51
|
+
|
|
52
|
+
`register`/`login` set the session cookies; to *render* login state on the client, use the generated
|
|
53
|
+
`getUser()` (emitted from the built-in `@user`). It reads the **readable** `__Secure-toil_user` companion
|
|
54
|
+
cookie and returns the typed user or `null` — instant, no network call. It is **display-only**; the server
|
|
55
|
+
still enforces access via `@auth`, so never trust it for authorization.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { getUser } from 'shared/server'; // generated; typed to the built-in @user
|
|
59
|
+
|
|
60
|
+
function Nav() {
|
|
61
|
+
const user = getUser(); // { toilUserId, username } | null
|
|
62
|
+
return user
|
|
63
|
+
? <span>Hi {user.username} <button onClick={logout}>Log out</button></span>
|
|
64
|
+
: <a href="/login">Log in</a>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function logout() {
|
|
68
|
+
await fetch('/auth/logout', { method: 'POST' });
|
|
69
|
+
location.href = '/login'; // cookies are cleared; bounce to login
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Gate a whole client route by redirecting when there's no session:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
export default function Dashboard() {
|
|
77
|
+
const user = getUser();
|
|
78
|
+
if (user == null) { location.href = '/login'; return null; } // not logged in -> to /login
|
|
79
|
+
return <main>Welcome, {user.username}</main>;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The authoritative check is still the server: any data this page fetches should come from an `@auth` route,
|
|
84
|
+
so a user who forged/deleted the readable cookie sees the redirect OR a `401`, never real data.
|
|
85
|
+
|
|
86
|
+
## 2c. Handling errors
|
|
87
|
+
|
|
88
|
+
`Auth.register` / `Auth.login` reject with a message you can show. The important distinguishable cases:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
try {
|
|
92
|
+
await Auth.register(username, password);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
const m = String(e);
|
|
95
|
+
if (m.includes('already registered')) setError('That username is taken — log in instead.');
|
|
96
|
+
else setError('Could not register, try again.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await Auth.login(username, password);
|
|
101
|
+
} catch {
|
|
102
|
+
// Wrong password OR unknown user both fail generically (anti-enumeration) — one message.
|
|
103
|
+
setError('Incorrect username or password.');
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Username taken** → `register` throws `auth: username already registered (log in instead)` (a
|
|
108
|
+
distinguishable case so you can guide the user).
|
|
109
|
+
- **Wrong password / unknown user** → `login` throws generically (`auth: request failed`) — by design,
|
|
110
|
+
the two are indistinguishable, so use ONE "incorrect username or password" message.
|
|
111
|
+
- **Rate limited** → after 5 attempts / 60s a `429` surfaces as the same generic throw; back off and tell
|
|
112
|
+
the user to wait.
|
|
113
|
+
|
|
114
|
+
## 3. Guard your routes — `@auth`
|
|
115
|
+
|
|
116
|
+
Put `@auth` on a route method or a whole `@rest` class. The generated dispatcher checks for a valid signed
|
|
117
|
+
session **before** your handler runs and returns `401` otherwise. `@auth` is unchanged by built-in auth —
|
|
118
|
+
it's the same decorator you'd use with hand-written auth.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
@rest('account')
|
|
122
|
+
class AccountApi {
|
|
123
|
+
@auth // this route needs a session
|
|
124
|
+
@get('/settings')
|
|
125
|
+
public settings(): Response { /* … */ }
|
|
126
|
+
|
|
127
|
+
@get('/public') // this one is open
|
|
128
|
+
public open(): Response { /* … */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@auth // …or guard the ENTIRE class
|
|
132
|
+
@rest('admin')
|
|
133
|
+
class AdminApi { /* every route requires a session */ }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 4. Read the current user
|
|
137
|
+
|
|
138
|
+
Inside any handler:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// The typed logged-in user (the built-in `@user`: toilUserId + username), or null.
|
|
142
|
+
const user = AuthService.getUser();
|
|
143
|
+
if (user != null) {
|
|
144
|
+
user.username; // string
|
|
145
|
+
user.toilUserId; // Uint8Array(32) — the stable id bytes
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The stable identity as a ToilUserId (gate on hasSession() first — see note).
|
|
149
|
+
if (AuthService.hasSession()) {
|
|
150
|
+
const id = AuthService.userId()!; // ToilUserId
|
|
151
|
+
// key your own data on id (see Extending)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> `ToilUserId` overloads `==`, so `AuthService.userId() == null` does not type-check. Gate with
|
|
156
|
+
> `AuthService.hasSession()` and then use `userId()!`, or use `getUser()` and null-check that.
|
|
157
|
+
|
|
158
|
+
## 5. The endpoint wire contract
|
|
159
|
+
|
|
160
|
+
You normally use the `Auth` client, but the raw endpoints are binary (`DataWriter`/`DataReader`, never
|
|
161
|
+
JSON):
|
|
162
|
+
|
|
163
|
+
| Route | Request body | Response |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `POST /auth/register/start` | `str(username) bytes(blinded)` | `u8(0) u32(mem) u32(iters) u32(par) bytes(salt) bytes(evaluated)` |
|
|
166
|
+
| `POST /auth/register/finish` | `str(username) bytes(pubkey) bytes(proof)` | `u8(status)` — `0` ok, `1` username taken |
|
|
167
|
+
| `POST /auth/login/start` | `str(username) bytes(blinded)` | `bytes(cid) str(aud) u32(mem) u32(iters) u32(par) bytes(salt) bytes(nonce) u64(iat) u64(exp) bytes(evaluated)` |
|
|
168
|
+
| `POST /auth/login/finish` | `bytes(cid) bytes(ct) bytes(sig)` | `u8(0) bytes(sessionToken) bytes(serverConfirm)` + `Set-Cookie`, or `u8(≠0)` on failure |
|
|
169
|
+
| `GET /auth/me` *(`@auth`)* | — | `bytes(toilUserId) str(username)` |
|
|
170
|
+
| `POST /auth/logout` *(`@auth`)* | — | `200` + cookie-clearing `Set-Cookie` |
|
|
171
|
+
|
|
172
|
+
Rate limiting: every register/login POST carries `@ratelimit(SlidingWindow, 5, 60)` (5 requests / 60s per
|
|
173
|
+
client) out of the box, so brute-force is throttled before it reaches the crypto.
|
|
174
|
+
|
|
175
|
+
## 6. Under `toiljs dev`
|
|
176
|
+
|
|
177
|
+
Everything runs locally with **zero setup**: the dev server emulates the ToilDB account/challenge storage
|
|
178
|
+
and the ML-DSA/ML-KEM/OPRF host functions in process, and the auth secrets fall back to insecure DEV
|
|
179
|
+
values. Register and login span requests (the accounts persist for the dev session). You'll see a warning
|
|
180
|
+
that `AUTH_SESSION_SECRET` is unset — that's expected in dev; set it before you deploy (see
|
|
181
|
+
[Configuration](./configuration.md)).
|
|
182
|
+
|
|
183
|
+
## 7. `Server.REST.auth.*`
|
|
184
|
+
|
|
185
|
+
Because the controller is a normal `@rest('auth')` class, a typed `Server.REST.auth.*` fetch client is
|
|
186
|
+
generated for free (`me`, `logout`, and the register/login methods). Use it for `/me` and `/logout`; but
|
|
187
|
+
**drive register/login through `toiljs/client` `Auth`**, not the generated client — only the `Auth` helper
|
|
188
|
+
runs the required browser-side OPRF/Argon2id/ML-DSA/ML-KEM crypto.
|
package/docs/auth.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Auth, sessions, and `@user`
|
|
2
2
|
|
|
3
|
+
> **Just want login working?** Enable **built-in auth** with one line — `server: { auth: true }` — and
|
|
4
|
+
> you get the whole `/auth/*` API + sessions with no code. Start at **[the auth guide](./auth/index.md)**
|
|
5
|
+
> ([how it works](./auth/how-it-works.md) · [usage](./auth/usage.md) · [configuration](./auth/configuration.md) ·
|
|
6
|
+
> [extending](./auth/extending.md)). This page is the deeper reference on the underlying primitives, used
|
|
7
|
+
> both by built-in auth and when you hand-write your own.
|
|
8
|
+
|
|
3
9
|
toiljs ships **Toil PQ-Auth**: a post-quantum password login where the password
|
|
4
10
|
never leaves the browser and the server stores only public verifier material.
|
|
5
11
|
On top of it sit HMAC-signed session cookies, a `@auth` route guard, and a
|
|
@@ -1,53 +1,191 @@
|
|
|
1
|
-
// Demo of the per-domain Analytics API
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// Demo of the per-domain Analytics API + the historical time-series (graphs).
|
|
2
|
+
//
|
|
3
|
+
// Server.REST.analytics.self() -> the current snapshot (typed getters)
|
|
4
|
+
// Server.REST.analytics.series({ query: { metric, range } }) -> a metric's history for a range
|
|
5
|
+
//
|
|
6
|
+
// Both are the real, typed fetch clients generated from the `@rest` controller in
|
|
7
|
+
// server/routes/Analytics.ts, which reads the toilscript `Analytics.self()` / `Analytics.series()` API.
|
|
8
|
+
// Under `toiljs dev` the dev server returns sample data; at the edge the SAME code reads the real
|
|
9
|
+
// metering rings. The chart is a self-contained inline SVG (no chart library).
|
|
10
|
+
import { useEffect, useState } from 'react';
|
|
6
11
|
|
|
7
|
-
import { SiteAnalytics } from 'shared/server';
|
|
12
|
+
import { SeriesData, SiteAnalytics } from 'shared/server';
|
|
8
13
|
|
|
9
14
|
export const metadata: Toil.Metadata = {
|
|
10
15
|
title: 'Analytics',
|
|
11
|
-
description: "This site's own analytics via the toilscript Analytics
|
|
16
|
+
description: "This site's own analytics + historical graphs via the toilscript Analytics API."
|
|
12
17
|
};
|
|
13
18
|
|
|
19
|
+
// MetricId (mirrors the server enum). Only the graph-worthy ones are listed for the picker.
|
|
20
|
+
const METRICS = [
|
|
21
|
+
{ id: 0, label: 'Requests', unit: 'count' },
|
|
22
|
+
{ id: 1, label: 'Bytes out (L1)', unit: 'bytes' },
|
|
23
|
+
{ id: 2, label: 'Bytes in (L1)', unit: 'bytes' },
|
|
24
|
+
{ id: 12, label: 'Gas used', unit: 'count' },
|
|
25
|
+
{ id: 13, label: 'DB ops', unit: 'count' },
|
|
26
|
+
{ id: 26, label: 'Stream bytes out', unit: 'bytes' },
|
|
27
|
+
{ id: 39, label: 'Memory bandwidth', unit: 'bytes' },
|
|
28
|
+
{ id: 41, label: 'Cache hits', unit: 'count' },
|
|
29
|
+
{ id: 42, label: 'Cache misses', unit: 'count' },
|
|
30
|
+
{ id: 43, label: 'Connected streams', unit: 'count' },
|
|
31
|
+
{ id: 45, label: 'Committed memory', unit: 'bytes' }
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
// AnalyticsRange (mirrors the server enum).
|
|
35
|
+
const RANGES = [
|
|
36
|
+
{ id: 0, label: '1h' },
|
|
37
|
+
{ id: 1, label: '6h' },
|
|
38
|
+
{ id: 3, label: '24h' },
|
|
39
|
+
{ id: 5, label: '7d' },
|
|
40
|
+
{ id: 7, label: '30d' }
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
type Unit = 'count' | 'bytes';
|
|
44
|
+
|
|
45
|
+
function fmt(n: number, unit: Unit): string {
|
|
46
|
+
if (unit === 'bytes') {
|
|
47
|
+
const u = ['B', 'KB', 'MB', 'GB'];
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (n >= 1024 && i < u.length - 1) {
|
|
50
|
+
n /= 1024;
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
return `${n.toFixed(i === 0 ? 0 : 1)} ${u[i]}`;
|
|
54
|
+
}
|
|
55
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A dependency-free SVG line chart of the per-second rate (value / bucketSecs). */
|
|
59
|
+
function LineChart({ points, bucketSecs, unit }: { points: number[]; bucketSecs: number; unit: Unit }) {
|
|
60
|
+
const W = 640;
|
|
61
|
+
const H = 220;
|
|
62
|
+
const pad = 40;
|
|
63
|
+
const rate = points.map((v) => (bucketSecs > 0 ? v / bucketSecs : 0));
|
|
64
|
+
const max = Math.max(1, ...rate);
|
|
65
|
+
const n = rate.length;
|
|
66
|
+
const x = (i: number) => pad + (n <= 1 ? 0 : (i / (n - 1)) * (W - 2 * pad));
|
|
67
|
+
const y = (v: number) => H - pad - (v / max) * (H - 2 * pad);
|
|
68
|
+
const line = rate.map((v, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
|
69
|
+
const area = `${line} L${x(n - 1).toFixed(1)},${H - pad} L${x(0).toFixed(1)},${H - pad} Z`;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<svg
|
|
73
|
+
viewBox={`0 0 ${W} ${H}`}
|
|
74
|
+
width="100%"
|
|
75
|
+
role="img"
|
|
76
|
+
aria-label="time series chart"
|
|
77
|
+
style={{ maxWidth: W, background: '#0b0f19', borderRadius: 8 }}>
|
|
78
|
+
<line x1={pad} y1={H - pad} x2={W - pad} y2={H - pad} stroke="#33415580" />
|
|
79
|
+
<line x1={pad} y1={pad} x2={pad} y2={H - pad} stroke="#33415580" />
|
|
80
|
+
<text x={6} y={pad + 4} fill="#94a3b8" fontSize={11}>
|
|
81
|
+
{fmt(max, unit)}/s
|
|
82
|
+
</text>
|
|
83
|
+
<text x={6} y={H - pad} fill="#94a3b8" fontSize={11}>
|
|
84
|
+
0
|
|
85
|
+
</text>
|
|
86
|
+
{n > 0 && <path d={area} fill="#38bdf822" />}
|
|
87
|
+
{n > 1 && <path d={line} fill="none" stroke="#38bdf8" strokeWidth={2} />}
|
|
88
|
+
{n === 1 && <circle cx={x(0)} cy={y(rate[0])} r={3} fill="#38bdf8" />}
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
14
93
|
export default function AnalyticsDemo() {
|
|
15
94
|
const [stats, setStats] = useState<SiteAnalytics | null>(null);
|
|
95
|
+
const [series, setSeries] = useState<SeriesData | null>(null);
|
|
96
|
+
const [metric, setMetric] = useState(0);
|
|
97
|
+
const [range, setRange] = useState(3);
|
|
16
98
|
const [err, setErr] = useState('');
|
|
17
99
|
|
|
18
|
-
const
|
|
100
|
+
const loadStats = async () => {
|
|
19
101
|
try {
|
|
20
|
-
setStats(await Server.REST.
|
|
102
|
+
setStats(await Server.REST.analyticsRoutes.self());
|
|
21
103
|
setErr('');
|
|
22
104
|
} catch (e) {
|
|
23
105
|
setErr(parseError(e));
|
|
24
106
|
}
|
|
25
107
|
};
|
|
26
108
|
|
|
109
|
+
// Refetch the graph whenever the metric or range changes.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
Server.REST.analyticsRoutes
|
|
112
|
+
.series({ query: { metric, range } })
|
|
113
|
+
.then(setSeries)
|
|
114
|
+
.catch((e: unknown) => setErr(parseError(e)));
|
|
115
|
+
}, [metric, range]);
|
|
116
|
+
|
|
27
117
|
const cap = (used: bigint, max: bigint) => `${used} / ${max ? String(max) : '∞'}`;
|
|
118
|
+
const meta = METRICS.find((m) => m.id === metric) ?? METRICS[0];
|
|
119
|
+
const points = series ? series.points.map(Number) : [];
|
|
28
120
|
|
|
29
121
|
return (
|
|
30
122
|
<main>
|
|
31
123
|
<h1>Analytics</h1>
|
|
32
124
|
<p>
|
|
33
|
-
<code>
|
|
34
|
-
<code>
|
|
35
|
-
|
|
125
|
+
<code>analytics.self()</code> reads this site's snapshot; <code>analytics.series(metric, range)</code>{' '}
|
|
126
|
+
reads the historical rings (30-day retention). Under <code>toiljs dev</code> you get sample data; at
|
|
127
|
+
the edge it is the real metering, same code.
|
|
36
128
|
</p>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
129
|
+
|
|
130
|
+
<section>
|
|
131
|
+
<h2>History</h2>
|
|
132
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', marginBottom: 8 }}>
|
|
133
|
+
<label>
|
|
134
|
+
Metric{' '}
|
|
135
|
+
<select value={metric} onChange={(e) => setMetric(Number(e.currentTarget.value))}>
|
|
136
|
+
{METRICS.map((m) => (
|
|
137
|
+
<option key={m.id} value={m.id}>
|
|
138
|
+
{m.label}
|
|
139
|
+
</option>
|
|
140
|
+
))}
|
|
141
|
+
</select>
|
|
142
|
+
</label>
|
|
143
|
+
<label>
|
|
144
|
+
Range{' '}
|
|
145
|
+
<select value={range} onChange={(e) => setRange(Number(e.currentTarget.value))}>
|
|
146
|
+
{RANGES.map((r) => (
|
|
147
|
+
<option key={r.id} value={r.id}>
|
|
148
|
+
{r.label}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
</label>
|
|
153
|
+
<span style={{ color: '#64748b' }}>
|
|
154
|
+
{series ? `${series.points.length} buckets · ${series.bucketSecs}s each` : ''}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
<LineChart points={points} bucketSecs={series?.bucketSecs ?? 3600} unit={meta.unit} />
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<section style={{ marginTop: 24 }}>
|
|
161
|
+
<h2>Snapshot</h2>
|
|
162
|
+
<p style={{ color: '#64748b', marginTop: 0 }}>
|
|
163
|
+
All <code>total*</code> values are all-time cumulative totals (since the site was first seen);
|
|
164
|
+
the <code>live</code> gauges are the current level, and the windows are the current rate-limit
|
|
165
|
+
usage vs the plan cap (cap ∞ = unlimited).
|
|
166
|
+
</p>
|
|
167
|
+
<button onClick={loadStats}>Load my site analytics</button>
|
|
168
|
+
{err && <p style={{ color: '#f87171' }}>{err}</p>}
|
|
169
|
+
{stats && (
|
|
170
|
+
<ul>
|
|
171
|
+
<li>total requests: {String(stats.totalRequests)}</li>
|
|
172
|
+
<li>total bytes out (L1): {String(stats.totalBytesOutL1)}</li>
|
|
173
|
+
<li>total bytes in (L1): {String(stats.totalBytesInL1)}</li>
|
|
174
|
+
<li>total gas used: {String(stats.totalGasUsed)}</li>
|
|
175
|
+
<li>total 2xx responses: {String(stats.totalStatus2xx)}</li>
|
|
176
|
+
<li>total db ops: {String(stats.totalDbOps)}</li>
|
|
177
|
+
<li>total cache hits: {String(stats.totalCacheHits)}</li>
|
|
178
|
+
<li>total cache misses: {String(stats.totalCacheMisses)}</li>
|
|
179
|
+
<li>cache hit ratio: {(stats.cacheHitRatio * 100).toFixed(1)}%</li>
|
|
180
|
+
<li>connected streams (live): {String(stats.liveConnectedStreams)}</li>
|
|
181
|
+
<li>committed memory (live): {fmt(Number(stats.liveCommittedMemoryBytes), 'bytes')}</li>
|
|
182
|
+
<li>requests this minute: {cap(stats.requestsThisMinute, stats.requestsThisMinuteCap)}</li>
|
|
183
|
+
<li>requests today: {cap(stats.requestsToday, stats.requestsTodayCap)}</li>
|
|
184
|
+
</ul>
|
|
185
|
+
)}
|
|
186
|
+
</section>
|
|
187
|
+
|
|
188
|
+
<p style={{ marginTop: 24 }}>
|
|
51
189
|
<Toil.Link href="/">Back home</Toil.Link>
|
|
52
190
|
</p>
|
|
53
191
|
</main>
|
|
@@ -1,14 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* This site's own analytics snapshot, mapped 1:1 from the toilscript `Analytics.self()` frame.
|
|
3
|
+
*
|
|
4
|
+
* Every `total*` field is an ALL-TIME CUMULATIVE COUNT — a running total since the site was first
|
|
5
|
+
* seen, monotonically increasing, never reset by a query. They are NOT "last hour" or "last minute":
|
|
6
|
+
* for time-windowed history use the separate `SeriesData` time-series (`/series`).
|
|
7
|
+
*
|
|
8
|
+
* All counter fields are `u64` (the guest exposes `u64` getters, so the mapping needs zero casts). The
|
|
9
|
+
* `live*` gauges are the current instantaneous level (not a total). The request-window fields are the
|
|
10
|
+
* current rate-limit bucket usage paired with the plan cap (cap `0` = unlimited).
|
|
11
|
+
*/
|
|
3
12
|
@data
|
|
4
13
|
export class SiteAnalytics {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
// --- Requests / L1 edge (all-time totals) ---
|
|
15
|
+
/** Total HTTP requests served, all-time. */
|
|
16
|
+
totalRequests: u64 = 0;
|
|
17
|
+
/** Total response bytes sent from the L1 edge, all-time. */
|
|
18
|
+
totalBytesOutL1: u64 = 0;
|
|
19
|
+
/** Total request bytes received at the L1 edge, all-time. */
|
|
20
|
+
totalBytesInL1: u64 = 0;
|
|
21
|
+
/** Total 2xx responses, all-time. */
|
|
22
|
+
totalStatus2xx: u64 = 0;
|
|
23
|
+
/** Total 3xx responses, all-time. */
|
|
24
|
+
totalStatus3xx: u64 = 0;
|
|
25
|
+
/** Total 4xx responses, all-time. */
|
|
26
|
+
totalStatus4xx: u64 = 0;
|
|
27
|
+
/** Total 5xx responses, all-time. */
|
|
28
|
+
totalStatus5xx: u64 = 0;
|
|
29
|
+
/** Total static-asset hits (served without a wasm dispatch), all-time. */
|
|
30
|
+
totalStaticHits: u64 = 0;
|
|
31
|
+
/** Total wasm handler dispatches, all-time. */
|
|
32
|
+
totalWasmDispatches: u64 = 0;
|
|
33
|
+
/** Total requests rejected because the executor pool was full, all-time. */
|
|
34
|
+
totalExecutorFullRejects: u64 = 0;
|
|
35
|
+
/** Total requests rejected for an unknown host, all-time. */
|
|
36
|
+
totalUnknownHostRejects: u64 = 0;
|
|
37
|
+
/** Total requests rejected by rate limiting, all-time. */
|
|
38
|
+
totalRateLimitedRejects: u64 = 0;
|
|
39
|
+
/** Total wasm gas consumed across all dispatches, all-time. */
|
|
40
|
+
totalGasUsed: u64 = 0;
|
|
41
|
+
|
|
42
|
+
// --- Database (all-time totals) ---
|
|
43
|
+
/** Total ToilDB operations issued, all-time. */
|
|
44
|
+
totalDbOps: u64 = 0;
|
|
45
|
+
/** Total ToilDB read operations, all-time. */
|
|
46
|
+
totalDbReads: u64 = 0;
|
|
47
|
+
/** Total ToilDB write operations, all-time. */
|
|
48
|
+
totalDbWrites: u64 = 0;
|
|
49
|
+
/** Total ToilDB operations that errored, all-time. */
|
|
50
|
+
totalDbErrors: u64 = 0;
|
|
51
|
+
/** Summed host-observed ToilDB op latency in nanoseconds, all-time (divide by totalDbOps for a mean). */
|
|
52
|
+
totalDbLatencyNsSum: u64 = 0;
|
|
53
|
+
|
|
54
|
+
// --- Streams / WebTransport (all-time totals) ---
|
|
55
|
+
/** Total stream connections accepted, all-time. */
|
|
56
|
+
totalStreamAccepts: u64 = 0;
|
|
57
|
+
/** Total streams rejected because they landed on the wrong node, all-time. */
|
|
58
|
+
totalStreamRejectWrongNode: u64 = 0;
|
|
59
|
+
/** Total streams rejected for capacity, all-time. */
|
|
60
|
+
totalStreamRejectCapacity: u64 = 0;
|
|
61
|
+
/** Total streams rejected for a missing/invalid artifact, all-time. */
|
|
62
|
+
totalStreamRejectArtifact: u64 = 0;
|
|
63
|
+
/** Total streams rejected by the guest, all-time. */
|
|
64
|
+
totalStreamRejectGuest: u64 = 0;
|
|
65
|
+
/** Total stream guest traps, all-time. */
|
|
66
|
+
totalStreamTraps: u64 = 0;
|
|
67
|
+
/** Total streams closed for idle timeout, all-time. */
|
|
68
|
+
totalStreamIdleTimeouts: u64 = 0;
|
|
69
|
+
/** Total bytes received on streams, all-time. */
|
|
70
|
+
totalStreamBytesIn: u64 = 0;
|
|
71
|
+
/** Total bytes sent on streams, all-time. */
|
|
72
|
+
totalStreamBytesOut: u64 = 0;
|
|
73
|
+
/** Total stream backpressure events, all-time. */
|
|
74
|
+
totalStreamBackpressureEvents: u64 = 0;
|
|
75
|
+
/** Total clean stream closes, all-time. */
|
|
76
|
+
totalStreamCloses: u64 = 0;
|
|
77
|
+
/** Total stream disconnects (abrupt), all-time. */
|
|
78
|
+
totalStreamDisconnects: u64 = 0;
|
|
79
|
+
|
|
80
|
+
// --- Daemons / L4 (all-time totals) ---
|
|
81
|
+
/** Total daemon starts, all-time. */
|
|
82
|
+
totalDaemonStarts: u64 = 0;
|
|
83
|
+
/** Total daemon start failures, all-time. */
|
|
84
|
+
totalDaemonStartFailures: u64 = 0;
|
|
85
|
+
/** Total daemon ticks fired, all-time. */
|
|
86
|
+
totalDaemonTicksFired: u64 = 0;
|
|
87
|
+
/** Total daemon ticks skipped because this node was not the leader, all-time. */
|
|
88
|
+
totalDaemonTicksSkippedNotLeader: u64 = 0;
|
|
89
|
+
/** Total daemon ticks that failed, all-time. */
|
|
90
|
+
totalDaemonTicksFailed: u64 = 0;
|
|
91
|
+
/** Total daemon leadership acquisitions, all-time. */
|
|
92
|
+
totalDaemonLeaderAcquires: u64 = 0;
|
|
93
|
+
/** Total daemon leadership fences (lost leadership), all-time. */
|
|
94
|
+
totalDaemonLeaderFenced: u64 = 0;
|
|
95
|
+
/** Total daemon outbound http_call attempts, all-time. */
|
|
96
|
+
totalDaemonHttpCallAttempts: u64 = 0;
|
|
97
|
+
/** Total daemon outbound http_call failures, all-time. */
|
|
98
|
+
totalDaemonHttpCallFailures: u64 = 0;
|
|
99
|
+
|
|
100
|
+
// --- Memory / email / cache (all-time totals) ---
|
|
101
|
+
/** Total wasm linear-memory bytes grown, all-time. */
|
|
102
|
+
totalMemGrownBytes: u64 = 0;
|
|
103
|
+
/** Total emails sent, all-time. */
|
|
104
|
+
totalEmails: u64 = 0;
|
|
105
|
+
/** Total responses served from the edge cache, all-time. */
|
|
106
|
+
totalCacheHits: u64 = 0;
|
|
107
|
+
/** Total cacheable responses that missed the edge cache, all-time. */
|
|
108
|
+
totalCacheMisses: u64 = 0;
|
|
109
|
+
|
|
110
|
+
// --- Derived ---
|
|
111
|
+
/** Fraction of cacheable responses served from cache: hits / (hits + misses), in 0..1. 0 when there
|
|
112
|
+
* were no cacheable responses. */
|
|
113
|
+
cacheHitRatio: f64 = 0;
|
|
114
|
+
|
|
115
|
+
// --- Live gauges (current instantaneous level, NOT a total) ---
|
|
116
|
+
/** Streams currently connected right now. */
|
|
117
|
+
liveConnectedStreams: u64 = 0;
|
|
118
|
+
/** Wasm linear memory currently committed right now, in bytes. */
|
|
119
|
+
liveCommittedMemoryBytes: u64 = 0;
|
|
120
|
+
|
|
121
|
+
// --- Request windows: current rate-limit usage vs plan cap (cap 0 = unlimited) ---
|
|
122
|
+
/** Requests used in the current 1-minute window. */
|
|
123
|
+
requestsThisMinute: u64 = 0;
|
|
124
|
+
/** Plan cap for the 1-minute window (0 = unlimited). */
|
|
125
|
+
requestsThisMinuteCap: u64 = 0;
|
|
126
|
+
/** Requests used in the current 24-hour window. */
|
|
127
|
+
requestsToday: u64 = 0;
|
|
128
|
+
/** Plan cap for the 24-hour window (0 = unlimited). */
|
|
129
|
+
requestsTodayCap: u64 = 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One metric's historical TIME-SERIES for a range, from `Analytics.series(metric, range)`: the raw
|
|
133
|
+
* per-bucket totals (oldest→newest) plus the bucket width + newest-bucket end, so the client can draw a
|
|
134
|
+
* graph and derive per-second rates. This is per-bucket HISTORY — distinct from the all-time totals in
|
|
135
|
+
* `SiteAnalytics`. Demonstrates the typed series API. */
|
|
136
|
+
@data
|
|
137
|
+
export class SeriesData {
|
|
138
|
+
metric: i32 = 0;
|
|
139
|
+
bucketSecs: u32 = 0;
|
|
140
|
+
headMs: u64 = 0;
|
|
141
|
+
points: i64[] = [];
|
|
14
142
|
}
|