@user-hub/auth 0.1.2 → 0.1.4
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 +16 -5
- package/dist/cli.js +13 -2
- package/dist/convex-react.d.ts +1 -1
- package/dist/convex-react.js +14 -3
- package/dist/{convex-7n6dqf5r.js → convex-zgnvvwvn.js} +12 -3
- package/dist/react.d.ts +6 -0
- package/dist/react.js +3 -1
- package/package.json +2 -1
- package/skills/user-hub-auth/SKILL.md +34 -1
package/README.md
CHANGED
|
@@ -39,10 +39,10 @@ HUB_ADMIN_PASSWORD=…
|
|
|
39
39
|
Then, from your app's folder (`bunx` reads `.env` and `.env.local` automatically; with a secret manager, run it through that):
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
|
-
bunx hub-user register --name "Your App" --prod https://your-app.com --dev http://localhost:3000
|
|
42
|
+
bunx hub-user register --name "Your App" --new --prod https://your-app.com --dev http://localhost:3000
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
It prints your **app key** (made from the name) and the values to set. Not deployed yet? Leave out `--prod`: you can develop, and badge scans start reaching the app once you add it. Run it again with the same `--name` to update the URLs; a URL left out keeps the registered one.
|
|
45
|
+
It prints your **app key** (made from the name) and the values to set. Not deployed yet? Leave out `--prod`: you can develop, and badge scans start reaching the app once you add it. Run it again with the same `--name` to update the URLs; a URL left out keeps the registered one. `--new` refuses to touch an app that already has that name, so you never take over someone else's; leave it out when updating your own app later. (A hub admin can do the same under **Apps** in the hub, and copy the values from **Setup**.)
|
|
46
46
|
|
|
47
47
|
### 3. Set the values
|
|
48
48
|
|
|
@@ -109,6 +109,17 @@ Run your dev server as usual and open it. With no token yet, it sends you to the
|
|
|
109
109
|
- **Different port:** run `hub-user register` again with the new `--dev` URL. The hub only sends tokens to registered URLs.
|
|
110
110
|
- **Production:** someone without a token sees "Scan your badge to sign in". Pass `signedOut={…}` to `<HubSignedIn>` to style that screen.
|
|
111
111
|
|
|
112
|
+
## Sign out
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useHubSignOut } from "@user-hub/auth/react"; // also from /convex-react
|
|
116
|
+
|
|
117
|
+
const signOut = useHubSignOut();
|
|
118
|
+
<button onClick={signOut}>Sign out</button>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
It forgets this app's token; other apps stay signed in. In production the person sees "Scan your badge to sign in". On a local dev server the hub opens so you can pick who to be next.
|
|
122
|
+
|
|
112
123
|
## Facilitator pages
|
|
113
124
|
|
|
114
125
|
People are members or facilitators. Always check on the server, then hide what members cannot use:
|
|
@@ -158,9 +169,9 @@ Apps without Convex use `HubAuthProvider` and `HubSignedIn` from `@user-hub/auth
|
|
|
158
169
|
| `@user-hub/auth/convex-react` | `ConvexHubAuthProvider` | For Convex apps, in place of `ConvexProvider`, inside `<body>`. Props: `client`, `app`, `hubUrl?` |
|
|
159
170
|
| `@user-hub/auth/react` | `HubAuthProvider` | The same without Convex: `app`, `hubUrl?` |
|
|
160
171
|
| both | `HubSignedIn` | Shows its children only when signed in. Props: `signedOut?`, `loading?` |
|
|
161
|
-
| | `useHubUser()`, `useHubToken()` | The person (or null), the raw token (or null). Also from `/convex-react` |
|
|
172
|
+
| | `useHubUser()`, `useHubToken()`, `useHubSignOut()` | The person (or null), the raw token (or null), a sign-out function. Also from `/convex-react` |
|
|
162
173
|
| `@user-hub/auth/server` | `verifyHubToken(requestOrToken, { app })` | Checks a token anywhere else |
|
|
163
|
-
| CLI | `hub-user register --name <name> [--prod <url>] [--dev <url>] [--hub <url>] [--json]` | Registers or updates the app (at least one URL) |
|
|
174
|
+
| CLI | `hub-user register --name <name> [--prod <url>] [--dev <url>] [--new] [--hub <url>] [--json]` | Registers or updates the app (at least one URL); `--new` only creates, exit code 3 if the name is taken |
|
|
164
175
|
|
|
165
176
|
## Developing this package
|
|
166
177
|
|
|
@@ -171,4 +182,4 @@ bun run check # Biome
|
|
|
171
182
|
bun run build # dist/: commit it
|
|
172
183
|
```
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
Release with `just release patch` (or `minor` / `major`): it bumps the version, publishes to npm and pushes the commit and tag. See the comments in the `justfile`.
|
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
var USAGE = `Usage: hub-user register --name <name> [--prod <url>] [--dev <url>] [--hub <url>] [--json]
|
|
5
|
+
var USAGE = `Usage: hub-user register --name <name> [--prod <url>] [--dev <url>] [--new] [--hub <url>] [--json]
|
|
6
6
|
|
|
7
7
|
--name The app's name in the hub; its key comes from it (required)
|
|
8
8
|
--prod Where badge scans send people. Leave it out until the app is
|
|
9
9
|
deployed: it can be developed, but not scheduled.
|
|
10
10
|
--dev Where developers open the app as a person, e.g. http://localhost:3000
|
|
11
|
+
--new Only create a new app: if the name is taken, change nothing and
|
|
12
|
+
exit with code 3 so you can pick another name
|
|
11
13
|
--hub The hub's URL (default: HUB_URL)
|
|
12
14
|
--json Print JSON instead of text
|
|
13
15
|
|
|
@@ -30,6 +32,7 @@ var { values, positionals } = parseArgs({
|
|
|
30
32
|
name: { type: "string" },
|
|
31
33
|
hub: { type: "string" },
|
|
32
34
|
json: { type: "boolean", default: false },
|
|
35
|
+
new: { type: "boolean", default: false },
|
|
33
36
|
help: { type: "boolean", short: "h", default: false }
|
|
34
37
|
}
|
|
35
38
|
});
|
|
@@ -57,10 +60,18 @@ var response = await fetch(`${hub}/api/apps/register`, {
|
|
|
57
60
|
body: JSON.stringify({
|
|
58
61
|
name,
|
|
59
62
|
...values.prod !== undefined && { url: values.prod },
|
|
60
|
-
...values.dev !== undefined && { devUrl: values.dev }
|
|
63
|
+
...values.dev !== undefined && { devUrl: values.dev },
|
|
64
|
+
...values.new && { createOnly: true }
|
|
61
65
|
})
|
|
62
66
|
}).catch((error) => fail(`could not reach ${hub}: ${error.message}`));
|
|
63
67
|
var result = await response.json().catch(() => ({}));
|
|
68
|
+
if (result.taken) {
|
|
69
|
+
if (values.json)
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
else
|
|
72
|
+
console.error(`hub-user: ${result.error}`);
|
|
73
|
+
process.exit(3);
|
|
74
|
+
}
|
|
64
75
|
if (!response.ok) {
|
|
65
76
|
fail(result.error ?? `the hub answered ${response.status}`);
|
|
66
77
|
}
|
package/dist/convex-react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ConvexReactClient } from "convex/react";
|
|
2
2
|
import { type HubAuthProviderProps } from "./react";
|
|
3
|
-
export { HubSignedIn, useHubSession, useHubToken, useHubUser, } from "./react";
|
|
3
|
+
export { HubSignedIn, useHubSession, useHubSignOut, useHubToken, useHubUser, } from "./react";
|
|
4
4
|
export type { HubRole, HubUser } from "./token";
|
|
5
5
|
/**
|
|
6
6
|
* HubAuthProvider for apps on Convex: the hub token becomes the Convex
|
package/dist/convex-react.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HubAuthProvider2,
|
|
3
3
|
HubSignedIn2,
|
|
4
|
+
useHubSignOut2,
|
|
4
5
|
useHubSession2,
|
|
5
6
|
useHubUser2,
|
|
6
7
|
useHubToken2
|
|
7
|
-
} from "./convex-
|
|
8
|
+
} from "./convex-zgnvvwvn.js";
|
|
8
9
|
|
|
9
10
|
// src/convex-react.tsx
|
|
10
11
|
import { ConvexProviderWithAuth } from "convex/react";
|
|
@@ -12,11 +13,20 @@ import { useMemo } from "react";
|
|
|
12
13
|
import { jsx } from "react/jsx-runtime";
|
|
13
14
|
function useConvexHubAuth() {
|
|
14
15
|
const session = useHubSession2();
|
|
16
|
+
const signOut = useHubSignOut2();
|
|
15
17
|
return useMemo(() => ({
|
|
16
18
|
isLoading: session === undefined,
|
|
17
19
|
isAuthenticated: Boolean(session),
|
|
18
|
-
fetchAccessToken: async (
|
|
19
|
-
|
|
20
|
+
fetchAccessToken: async ({
|
|
21
|
+
forceRefreshToken
|
|
22
|
+
}) => {
|
|
23
|
+
if (forceRefreshToken) {
|
|
24
|
+
signOut();
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return session && session.user.expiresAt > Date.now() ? session.token : null;
|
|
28
|
+
}
|
|
29
|
+
}), [session, signOut]);
|
|
20
30
|
}
|
|
21
31
|
function ConvexHubAuthProvider({
|
|
22
32
|
client,
|
|
@@ -36,6 +46,7 @@ export {
|
|
|
36
46
|
ConvexHubAuthProvider,
|
|
37
47
|
HubSignedIn2 as HubSignedIn,
|
|
38
48
|
useHubSession2 as useHubSession,
|
|
49
|
+
useHubSignOut2 as useHubSignOut,
|
|
39
50
|
useHubToken2 as useHubToken,
|
|
40
51
|
useHubUser2 as useHubUser
|
|
41
52
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/react.tsx
|
|
2
2
|
import {
|
|
3
3
|
createContext,
|
|
4
|
+
useCallback,
|
|
4
5
|
useContext,
|
|
5
6
|
useEffect,
|
|
6
7
|
useMemo,
|
|
@@ -41,7 +42,8 @@ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
|
41
42
|
var LOCAL_HOST = /^(localhost|127(\.\d{1,3}){3}|10(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2\d|3[01])(\.\d{1,3}){2}|\[::1\]|[a-z0-9-]+\.local)$/i;
|
|
42
43
|
var HubContext = createContext({
|
|
43
44
|
session: undefined,
|
|
44
|
-
redirecting: false
|
|
45
|
+
redirecting: false,
|
|
46
|
+
signOut: () => {}
|
|
45
47
|
});
|
|
46
48
|
var storageKey = (app) => `user-hub-token:${app}`;
|
|
47
49
|
function readStorage(key) {
|
|
@@ -95,7 +97,11 @@ function HubAuthProvider2({
|
|
|
95
97
|
window.location.replace(`${hubUrl.replace(/\/+$/, "")}/open/${encodeURIComponent(app)}`);
|
|
96
98
|
}
|
|
97
99
|
}, [redirecting, hubUrl, app]);
|
|
98
|
-
const
|
|
100
|
+
const signOut = useCallback(() => {
|
|
101
|
+
writeStorage(storageKey(app), null);
|
|
102
|
+
setSession(null);
|
|
103
|
+
}, [app]);
|
|
104
|
+
const state = useMemo(() => ({ session, redirecting, signOut }), [session, redirecting, signOut]);
|
|
99
105
|
return /* @__PURE__ */ jsx(HubContext.Provider, {
|
|
100
106
|
value: state,
|
|
101
107
|
children
|
|
@@ -117,6 +123,9 @@ function HubSignedIn2({
|
|
|
117
123
|
return loading;
|
|
118
124
|
return session ? children : signedOut;
|
|
119
125
|
}
|
|
126
|
+
function useHubSignOut2() {
|
|
127
|
+
return useContext(HubContext).signOut;
|
|
128
|
+
}
|
|
120
129
|
function useHubSession2() {
|
|
121
130
|
return useContext(HubContext).session;
|
|
122
131
|
}
|
|
@@ -128,4 +137,4 @@ function useHubToken2() {
|
|
|
128
137
|
return useHubSession2()?.token ?? null;
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
export { HubAuthProvider2, HubSignedIn2, useHubSession2, useHubUser2, useHubToken2 };
|
|
140
|
+
export { HubAuthProvider2, HubSignedIn2, useHubSignOut2, useHubSession2, useHubUser2, useHubToken2 };
|
package/dist/react.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export declare function HubSignedIn({ children, signedOut, loading, }: {
|
|
|
22
22
|
signedOut?: ReactNode;
|
|
23
23
|
loading?: ReactNode;
|
|
24
24
|
}): ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Signs the person out of this app: `const signOut = useHubSignOut()`. They
|
|
27
|
+
* need to scan their badge again (or, on a dev server, pick a person at the
|
|
28
|
+
* hub). Other apps stay signed in.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useHubSignOut(): () => void;
|
|
25
31
|
/** The current session: undefined while loading, null when signed out. */
|
|
26
32
|
export declare function useHubSession(): HubSession | null | undefined;
|
|
27
33
|
/** The signed-in person, or null. */
|
package/dist/react.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HubAuthProvider2,
|
|
3
3
|
HubSignedIn2,
|
|
4
|
+
useHubSignOut2,
|
|
4
5
|
useHubSession2,
|
|
5
6
|
useHubUser2,
|
|
6
7
|
useHubToken2
|
|
7
|
-
} from "./convex-
|
|
8
|
+
} from "./convex-zgnvvwvn.js";
|
|
8
9
|
export {
|
|
9
10
|
HubAuthProvider2 as HubAuthProvider,
|
|
10
11
|
HubSignedIn2 as HubSignedIn,
|
|
11
12
|
useHubSession2 as useHubSession,
|
|
13
|
+
useHubSignOut2 as useHubSignOut,
|
|
12
14
|
useHubToken2 as useHubToken,
|
|
13
15
|
useHubUser2 as useHubUser
|
|
14
16
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@user-hub/auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Sign people in from User Hub badges: Convex auth config, React provider, server verification and the hub-user CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@biomejs/biome": "2.5.15",
|
|
54
|
+
"@happy-dom/global-registrator": "^20.14.5",
|
|
54
55
|
"@types/bun": "^1.4.2",
|
|
55
56
|
"@types/react": "^19.3.0",
|
|
56
57
|
"@types/react-dom": "^19.3.0",
|
|
@@ -7,6 +7,26 @@ description: Sign people into an app from their User Hub badge. Use when an app
|
|
|
7
7
|
|
|
8
8
|
People at a workshop scan a printed QR badge. The hub sends them to whichever app is live, signed in, with a token in the URL fragment (`#token=…`) that only that app accepts. This package turns that token into a signed-in person. The app keeps its own data, keyed on the person's hub id.
|
|
9
9
|
|
|
10
|
+
## Starting a new app
|
|
11
|
+
|
|
12
|
+
If there is no app yet, don't build one by hand: scaffold one with sign-in already wired (TanStack Start + Convex, sign-in and sign-out, a facilitator page, `CLAUDE.md` and this skill):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun create @user-hub my-app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
In a terminal it asks for the hub's URL and admin password and saves them to the new app's `.env.local`. As an agent you never handle the password, so either ask the human to run it, or run it only when `HUB_URL` and `HUB_ADMIN_PASSWORD` are already in the environment (for example `infisical run -- bun create @user-hub my-app`).
|
|
19
|
+
|
|
20
|
+
Then:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
cd my-app
|
|
24
|
+
bun run setup # Convex project, hub registration, HUB_ISSUER and HUB_JWKS, once
|
|
25
|
+
bun run dev # http://localhost:3000
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That covers every step below; read on only to add User Hub to an existing app or to change the wiring.
|
|
29
|
+
|
|
10
30
|
## The hub's URL
|
|
11
31
|
|
|
12
32
|
The hub has one address, for example `https://hub.example.com`. The human gives it to you; you need it in three places, all with the same value:
|
|
@@ -38,9 +58,11 @@ You need four things. Ask for them; never guess.
|
|
|
38
58
|
2. **Register the app** from the app's repo root, with `HUB_URL` and the password supplied the way the human chose in question 3 above:
|
|
39
59
|
|
|
40
60
|
```sh
|
|
41
|
-
bunx hub-user register --name "<app name>" --prod https://app.example.com --dev http://localhost:3000
|
|
61
|
+
bunx hub-user register --name "<app name>" --new --prod https://app.example.com --dev http://localhost:3000
|
|
42
62
|
```
|
|
43
63
|
|
|
64
|
+
`--new` makes sure you never take over another app: if the name is already taken, nothing changes and it exits with code 3 and "already exists". Then ask the human for another name and run it again. Drop `--new` only when updating this app's own registration later.
|
|
65
|
+
|
|
44
66
|
Leave out `--prod` if the app is not deployed yet. Running it again with the same `--name` updates the URLs; a URL left out keeps the registered one. If it fails with "set HUB_URL", "set HUB_ADMIN_PASSWORD" or a wrong password, stop and ask the human; do not work around it.
|
|
45
67
|
|
|
46
68
|
It prints the app **key** (made from the name), `HUB_ISSUER` and `HUB_JWKS` (server side) and `VITE_HUB_URL` (frontend). Add `--json` to parse them.
|
|
@@ -94,6 +116,17 @@ Run the app as usual (`bun run dev`) on the `--dev` URL you registered. Opening
|
|
|
94
116
|
- If the dev server runs on another port, run `hub-user register` again with the same `--name` and the new `--dev` URL: the hub only sends tokens back to registered URLs.
|
|
95
117
|
- This only happens on local addresses (localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, *.local). In production a person without a token sees "Scan your badge to sign in".
|
|
96
118
|
|
|
119
|
+
## Sign out
|
|
120
|
+
|
|
121
|
+
Use `useHubSignOut()` from the same entry point as the provider; never clear storage or tokens yourself.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const signOut = useHubSignOut();
|
|
125
|
+
<button onClick={signOut}>Sign out</button>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It forgets this app's token only. In production the person then sees the signed-out screen and scans their badge again; on a local dev server the hub opens to pick another person.
|
|
129
|
+
|
|
97
130
|
## Facilitator pages
|
|
98
131
|
|
|
99
132
|
People are either `"member"` or `"facilitator"`, set in the hub. To give facilitators their own page or actions:
|