@close-by/clay 0.2.21 → 0.2.22
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/docs/api.md +3 -1
- package/docs/production.md +42 -0
- package/docs/protocol.md +8 -0
- package/package.json +5 -5
- package/src/index.ts +10 -0
package/docs/api.md
CHANGED
|
@@ -99,6 +99,7 @@ ui.run({
|
|
|
99
99
|
| `sessionAbsoluteMs` | `number` | | Optional: clear auth + redirect after this time since hello |
|
|
100
100
|
| `sessionExpiredPath` | `string` | `'/'` | SPA path after idle/absolute expiry |
|
|
101
101
|
| `resolveUserId` | `(ctx) => …` | cookie helper when `authSecret` set | Trusted identity on WebSocket hello |
|
|
102
|
+
| `routes` | `Record<path, (req, ctx) => Response>` | | Extra HTTP routes, exact path, any method. `ctx.signIn(userId, location)` / `ctx.signOut(location)` redirect and set/clear the auth cookie. Used for OAuth callbacks — see [Production — single sign-on](./production.md#single-sign-on-oidc) |
|
|
102
103
|
|
|
103
104
|
#### Auth cookie recipe
|
|
104
105
|
|
|
@@ -118,7 +119,7 @@ ui.establishAuthSession(userId, { path: '/account' });
|
|
|
118
119
|
ui.clearAuthSession({ path: '/login' });
|
|
119
120
|
```
|
|
120
121
|
|
|
121
|
-
Password hashing, login rate limits, and `requireAuth` / `requireRole` live in optional `@close-by/clay-auth`. See `/examples/auth
|
|
122
|
+
Password hashing, login rate limits, OIDC single sign-on (`createOidcLogin`), and `requireAuth` / `requireRole` live in optional `@close-by/clay-auth`. See `/examples/auth` and [Production — single sign-on](./production.md#single-sign-on-oidc).
|
|
122
123
|
|
|
123
124
|
Returns `ClayServer` with `.start()` / `.stop()` / `.port` (`.start()` is already called by `ui.run`).
|
|
124
125
|
|
|
@@ -1815,6 +1816,7 @@ onClick: async () => {
|
|
|
1815
1816
|
| `ui.updateUrlSearch(patch, opts?)` | `void` | Merge query keys; `null`/`''` deletes |
|
|
1816
1817
|
| `ui.urlState(defaults, opts?)` | reactive object | `ui.state` + hydrate/sync query string (prefer this) |
|
|
1817
1818
|
| `ui.openExternal(url)` | `void` | `window.open` in a new tab (`noopener`) |
|
|
1819
|
+
| `ui.redirect(url)` | `void` | Full-page navigation in the current tab (`location.assign`) — for sign-in routes / external pages; use `ui.navigate` for app pages |
|
|
1818
1820
|
| `ui.theme.set(mode)` | `void` | Push `light` \| `dark` \| `system` to this client |
|
|
1819
1821
|
| `ui.theme.get()` | `ThemeMode \| null` | Last value set on this session |
|
|
1820
1822
|
| `ui.theme.palette(name)` | `void` | Switch colour palette for this client (`kiln`, `cobalt`, `ocean`, `forest`, `orchid`, `citrus`, `graphite`, `neon`, `sky`, `navy`, `indigo`, `denim`, `midnight`, or a custom one); remembered across reloads |
|
package/docs/production.md
CHANGED
|
@@ -65,6 +65,48 @@ ui.page('/admin', () => {
|
|
|
65
65
|
|
|
66
66
|
Nav filtering is not authorization. Always guard pages that must not leak data.
|
|
67
67
|
|
|
68
|
+
## Single sign-on (OIDC)
|
|
69
|
+
|
|
70
|
+
`@close-by/clay-auth` signs users in with any OpenID Connect provider (authorization code + PKCE). The provider is only used to sign in: once its ID token is verified, Clay sets its usual signed auth cookie, so everything above (`resolveUserId`, guards, idle expiry) works unchanged and there are no provider tokens to refresh.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createOidcLogin } from '@close-by/clay-auth';
|
|
74
|
+
|
|
75
|
+
const sso = createOidcLogin({
|
|
76
|
+
issuer: process.env.OIDC_ISSUER!, // discovery: ${issuer}/.well-known/openid-configuration
|
|
77
|
+
clientId: process.env.OIDC_CLIENT_ID!,
|
|
78
|
+
clientSecret: process.env.OIDC_CLIENT_SECRET, // only for confidential clients
|
|
79
|
+
redirectUri: 'https://hub.example.com/auth/callback',
|
|
80
|
+
async onSignIn(claims) {
|
|
81
|
+
// Upsert your user; return the Clay user id, or null to refuse.
|
|
82
|
+
await users.upsert(claims.sub, { email: claims.email });
|
|
83
|
+
return claims.sub;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
ui.run({
|
|
88
|
+
authSecret: process.env.CLAY_AUTH_SECRET!,
|
|
89
|
+
routes: sso.routes({ afterLogin: '/', afterLogout: '/login', errorPath: '/login' }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Sign-in button (full page load — the route redirects to the provider):
|
|
93
|
+
ui.button('Sign in', { onClick: () => ui.redirect('/auth/login?returnTo=/reports') });
|
|
94
|
+
// Sign-out (clears the cookie, then signs out at the provider):
|
|
95
|
+
ui.button('Sign out', { onClick: () => ui.redirect('/auth/logout') });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Routes: `GET /auth/login` (accepts `?returnTo=/path`, same-origin paths only) → provider → `GET /auth/callback` → cookie set → `returnTo`. `GET /auth/logout` clears the cookie and goes to the provider's sign-out. Failures land on `errorPath?auth_error=<state|expired|invalid_token|denied|provider|…>`.
|
|
99
|
+
|
|
100
|
+
What is checked: `state` (one-shot HttpOnly cookie scoped to the callback path, 10 min), PKCE `S256`, ID token RS256 signature against the provider's JWKS (re-fetched on unknown `kid`), `iss`, `aud`, `exp`/`iat` (60 s skew), and `nonce`.
|
|
101
|
+
|
|
102
|
+
**Provider setup:** register an app/client that allows the authorization code flow with scopes `openid email profile`, callback URL `https://<host>/auth/callback`, and sign-out return URL `https://<host><afterLogout>`. Map roles from whatever claim your provider puts groups in, inside `onSignIn`.
|
|
103
|
+
|
|
104
|
+
**Sign-out:** uses the discovery `end_session_endpoint` when the provider has one. Otherwise pass `logoutUrl: (returnTo) => '…'` with the provider's sign-out URL; without either, sign-out only clears Clay's cookie.
|
|
105
|
+
|
|
106
|
+
**Behind an authenticating proxy:** if a reverse proxy or load balancer already signs users in and forwards a signed identity header, skip the routes and read (and verify) that header in `resolveUserId`.
|
|
107
|
+
|
|
108
|
+
The demo enables a "Sign in with SSO" button on `/examples/auth/login` when `OIDC_ISSUER` and `OIDC_CLIENT_ID` are set (see `apps/demo/src/examples/_oidc.ts` for the optional variables).
|
|
109
|
+
|
|
68
110
|
## Rate limits & abuse
|
|
69
111
|
|
|
70
112
|
Clay does not ship a global rate limiter. For handlers that trigger expensive work (ClickHouse queries, re-dispatch, file export):
|
package/docs/protocol.md
CHANGED
|
@@ -180,6 +180,14 @@ Client sets `location.hash` via `history.replaceState` (`hash` has no leading `#
|
|
|
180
180
|
|
|
181
181
|
Client sets `location.search` via `history.replaceState` or `pushState` (`mode`, default `replace`). `search` has no leading `?`; empty string clears. Pathname and hash are preserved. Hydrated the other way on `hello.search` and re-synced in place on `urlchange`. Prefer `ui.setUrlSearch` / `ui.updateUrlSearch` / `ui.getUrlSearch`. See [URL search](./url-search.md).
|
|
182
182
|
|
|
183
|
+
### `redirect`
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "op": "redirect", "url": "/auth/login?returnTo=/reports" }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Client runs `window.location.assign(url)` — a full page load that leaves the SPA. Prefer `ui.redirect`.
|
|
190
|
+
|
|
183
191
|
### `openExternal`
|
|
184
192
|
|
|
185
193
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@close-by/clay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"description": "NiceGUI-like ui.* facade for Clay apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@close-by/clay-compiler": "0.2.
|
|
41
|
-
"@close-by/clay-core": "0.2.
|
|
42
|
-
"@close-by/clay-components": "0.2.
|
|
43
|
-
"@close-by/clay-server": "0.2.
|
|
40
|
+
"@close-by/clay-compiler": "0.2.22",
|
|
41
|
+
"@close-by/clay-core": "0.2.22",
|
|
42
|
+
"@close-by/clay-components": "0.2.22",
|
|
43
|
+
"@close-by/clay-server": "0.2.22",
|
|
44
44
|
"@tailwindcss/cli": "^4.3.3",
|
|
45
45
|
"tailwindcss": "^4.3.3"
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -464,6 +464,7 @@ import {
|
|
|
464
464
|
setUrlSearch as setUrlSearchCore,
|
|
465
465
|
updateUrlSearch as updateUrlSearchCore,
|
|
466
466
|
openExternal as openExternalCore,
|
|
467
|
+
redirect as redirectCore,
|
|
467
468
|
scroll as scrollCore,
|
|
468
469
|
timer as timerCore,
|
|
469
470
|
storage as storageCore,
|
|
@@ -1681,6 +1682,14 @@ export function updateUrlSearch(
|
|
|
1681
1682
|
updateUrlSearchCore(patch, opts);
|
|
1682
1683
|
}
|
|
1683
1684
|
|
|
1685
|
+
/**
|
|
1686
|
+
* Full-page navigation in the current tab — for external sign-in pages or
|
|
1687
|
+
* server routes (`/auth/login`). Use `ui.navigate` for app pages.
|
|
1688
|
+
*/
|
|
1689
|
+
export function redirect(url: string): void {
|
|
1690
|
+
redirectCore(url);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1684
1693
|
/** Open a URL in a new tab. Prefer over `runJavaScript('window.open…')`. */
|
|
1685
1694
|
export function openExternal(url: string): void {
|
|
1686
1695
|
openExternalCore(url);
|
|
@@ -2025,6 +2034,7 @@ export const ui = {
|
|
|
2025
2034
|
setUrlSearch,
|
|
2026
2035
|
updateUrlSearch,
|
|
2027
2036
|
openExternal,
|
|
2037
|
+
redirect,
|
|
2028
2038
|
runJavaScript,
|
|
2029
2039
|
scroll,
|
|
2030
2040
|
timer,
|