create-feltkit 0.3.8 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-feltkit",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Create a FeltKit application: npm create feltkit@latest my-app (add -- --template managed for Managed FeltDB)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -226,28 +226,22 @@ without their FeltDB settings.
226
226
 
227
227
  Open <http://localhost:3000>.
228
228
 
229
- 4. **Sign in.** Production has no development sign-in (Ada and Grace are for development).
230
- Until you connect your own login, sign in with a session token pasted into the app's sign-in
231
- screen. On Vercel, `npx feltkit vercel deploy` printed yours. To sign in someone else (or
232
- yourself again), run this in the project's folder:
233
-
234
- ```bash
235
- npx feltkit session create --env production --type user --id you --role member
236
- ```
237
-
238
- When the deployment's data is in FeltDB (Vercel, `--target hosted`, `--target self-hosted`), it
239
- uses the deployment's settings from `.env.local` and the app keeps running; it refuses rather
240
- than write anywhere else when they are missing. For the Node run, use `.feltkit/deploy` with
241
- the app stopped (only one process may open its state). For the one-container Docker run, use a
242
- one-off container on the same volume while the app's container is stopped:
243
-
244
- ```bash
245
- docker run --rm -v tasks-data:/data tasks node node_modules/@feltdb/feltkit/bin/feltkit.js session create --env production --type user --id you --role member
246
- ```
229
+ 4. **Sign in.** Production has no development sign-in (Ada and Grace are for development). The
230
+ app has built-in accounts (`auth: { accounts: { roles: ["member"] } }` in `feltkit/app.ts`):
231
+ open it and create your account with an e-mail address and a password; anyone you share the
232
+ link with can do the same, and a new account is a member. Passwords are stored only as a
233
+ scrypt hash, in the app's own FeltDB. To close sign-up once your people have accounts, set
234
+ `signUp: false`. To give someone admin, or to use your own login instead, see
235
+ `auth.authenticate` and production Studio below.
236
+
237
+ The sign-in screen grows with configuration: add `email: { from: "auth@yourdomain" }` to
238
+ `auth.accounts` for "Forgot your password?" and address verification (development prints each
239
+ e-mail; production sends through your Mailerport service, `MAILPORT_URL` and
240
+ `MAILPORT_API_KEY`), and `auth.providers` for "Continue with Google / GitHub" (client id in
241
+ `feltkit/app.ts`, client secret as `FELTKIT_CREDENTIAL_<ID>_CLIENT_SECRET`).
247
242
 
248
243
  **On AppBoundry** (`npx feltkit build --target appboundry`, then `appboundry run tasks.app`),
249
- `feltkit session create` does not apply: the artifact runs without this project or FeltKit's
250
- CLI. AppBoundry's operators create users with your app's roles and issue their sessions:
244
+ AppBoundry's operators create users with your app's roles and issue their sessions:
251
245
 
252
246
  ```bash
253
247
  appboundry admin users add you --role member --state <installation>
@@ -256,8 +250,15 @@ without their FeltDB settings.
256
250
 
257
251
  See AppBoundry's production operations guide for bootstrapping the first operator.
258
252
 
259
- For real users, add `auth.authenticate` to `feltkit({ ... })` to verify requests with your
260
- identity provider.
253
+ Operators and scripts can still get a session token without an account:
254
+ `npx feltkit session create --env production --type user --id <id> --role <role>` in the
255
+ project's folder (it uses the deployment's FeltDB settings from `.env.local`). For the
256
+ one-container Docker run, use a one-off container on the same volume while the app's container
257
+ is stopped:
258
+
259
+ ```bash
260
+ docker run --rm -v tasks-data:/data tasks node node_modules/@feltdb/feltkit/bin/feltkit.js session create --env production --type user --id you --role member
261
+ ```
261
262
 
262
263
  5. **Manage it.** On Vercel, production Studio is on (at `/_feltkit/` of your app). Run
263
264
  `npx feltkit manage` for a one-time sign-in link, or use **Manage in production Studio** on
@@ -26,6 +26,9 @@ export const app = feltkit({
26
26
  // feltkit.operations.*: in production, admins are Studio's operators (npx feltkit operator grant).
27
27
  admin: ["intelligence.*", "credential.*", "configuration.*", "apikeys.*", "webhooks.*", "jobs.*", "schedules.*", "feltkit.operations.*"],
28
28
  },
29
+ // People sign up and sign in with an e-mail address and a password (stored in this app's FeltDB;
30
+ // only a hash of the password). A new account is a member.
31
+ auth: { accounts: { roles: ["member"] } },
29
32
  // Development-only identities. Sign in as them from the app or the dashboard.
30
33
  dev: {
31
34
  users: [
@@ -13,7 +13,7 @@
13
13
  "typecheck": "tsc --noEmit"
14
14
  },
15
15
  "dependencies": {
16
- "@feltdb/feltkit": "0.3.8",
16
+ "@feltdb/feltkit": "0.3.10",
17
17
  "react": "19.2.0",
18
18
  "react-dom": "19.2.0"
19
19
  },
@@ -19,7 +19,7 @@ function describe(error: unknown): string {
19
19
  }
20
20
 
21
21
  export function App() {
22
- const { session, signInAsDevUser, signOut } = useSession();
22
+ const { session, signInAsDevUser, signIn, signUp, signOut } = useSession();
23
23
  const live = useConnectionState();
24
24
  const [notice, setNotice] = useState<Notice>(null);
25
25
  const principal = session?.principal ?? null;
@@ -44,13 +44,24 @@ export function App() {
44
44
 
45
45
  const report = (error: unknown) => setNotice({ tone: "error", text: describe(error) });
46
46
 
47
+ // A link back into the app: a provider sign-in, a password reset, or an e-mail verification.
48
+ const client = useFeltKit();
49
+ const [resetToken, setResetToken] = useState<string | null>(null);
50
+ useEffect(() => {
51
+ void client.handleRedirect().then((r) => {
52
+ if (r?.kind === "reset") setResetToken(r.token);
53
+ else if (r?.kind === "verified") setNotice({ tone: "info", text: `${r.email} is verified.` });
54
+ else if (r?.kind === "error" || r?.kind === "verify-failed") setNotice({ tone: "error", text: r.message });
55
+ });
56
+ }, [client]);
57
+
47
58
  return (
48
59
  <div className="shell">
49
60
  <header className="topbar">
50
61
  <div className="brand">
51
62
  <img className="mark" src="/feltkit-mark.png" width="30" height="30" alt="" />
52
63
  <span className="brand-name">Tasks</span>
53
- {session && <span className={`env ${session.environment.production ? "env-prod" : ""}`}>{session.environment.name}</span>}
64
+ {session && !session.environment.production && <span className="env">{session.environment.name}</span>}
54
65
  </div>
55
66
  <div className="topbar-right">
56
67
  <span className={`live ${live ? "on" : ""}`} title={live ? "Live: changes from anyone appear here immediately" : "Connecting to the change stream…"}>
@@ -73,6 +84,11 @@ export function App() {
73
84
  </select>
74
85
  </label>
75
86
  )}
87
+ {principal && !session?.devUsers.length && (
88
+ <button className="ghost signout" onClick={() => void signOut().catch(report)} title={principal.displayName ?? principal.id}>
89
+ Sign out
90
+ </button>
91
+ )}
76
92
  <ThemeToggle />
77
93
  {session?.dashboard && (
78
94
  <a className="dashboard-link" href={session.dashboard} target="_blank" rel="noreferrer">
@@ -93,68 +109,180 @@ export function App() {
93
109
 
94
110
  {!session ? (
95
111
  <div className="center muted">Connecting…</div>
112
+ ) : resetToken ? (
113
+ <div className="center">
114
+ <div className="card narrow">
115
+ <ResetForm token={resetToken} onDone={() => (setResetToken(null), setNotice({ tone: "info", text: "Your password is set. Other sessions were signed out." }))} />
116
+ </div>
117
+ </div>
96
118
  ) : !principal ? (
97
- <SignedOut devUsers={session.devUsers} onSignIn={(id) => signInAsDevUser(id).catch(report)} />
119
+ <SignedOut
120
+ devUsers={session.devUsers}
121
+ accounts={session.accounts ?? null}
122
+ onDevSignIn={(id) => signInAsDevUser(id).catch(report)}
123
+ onSignIn={signIn}
124
+ onSignUp={signUp}
125
+ />
98
126
  ) : (
99
127
  <Workspace canWrite={canWrite} canComplete={canComplete} role={principal.roles.join(", ")} onError={report} />
100
128
  )}
101
129
 
102
- <footer className="footer">
103
- Every project, task, and note lives in this app’s single FeltDB instance. Changes made by other people, agents (through AppPort), and services appear here live.
104
- </footer>
130
+ {session && !session.environment.production && (
131
+ <footer className="footer">
132
+ Every project, task, and note lives in this app’s single FeltDB instance. Changes made by other people, agents (through AppPort), and services appear here live.
133
+ </footer>
134
+ )}
105
135
  </div>
106
136
  );
107
137
  }
108
138
 
109
- function SignedOut({ devUsers, onSignIn }: { devUsers: Array<{ id: string; name: string; roles: string[] }>; onSignIn: (id: string) => void }) {
139
+ function SignedOut(props: {
140
+ devUsers: Array<{ id: string; name: string; roles: string[] }>;
141
+ accounts: { signUp: boolean; email?: boolean; providers?: Array<{ id: string; name: string }> } | null;
142
+ onDevSignIn: (id: string) => void;
143
+ onSignIn: (input: { email: string; password: string }) => Promise<void>;
144
+ onSignUp: (input: { email: string; password: string; name?: string }) => Promise<void>;
145
+ }) {
110
146
  return (
111
147
  <div className="center">
112
148
  <div className="card narrow">
113
- <h1>Sign in</h1>
114
- {devUsers.length ? (
149
+ {props.devUsers.length ? (
115
150
  <>
151
+ <h1>Sign in</h1>
116
152
  <p className="muted">Development identities from feltkit/app.ts:</p>
117
153
  <div className="stack">
118
- {devUsers.map((u) => (
119
- <button key={u.id} className="secondary wide" onClick={() => onSignIn(u.id)}>
154
+ {props.devUsers.map((u) => (
155
+ <button key={u.id} className="secondary wide" onClick={() => props.onDevSignIn(u.id)}>
120
156
  {u.name} <span className="muted">· {u.roles.join(", ")}</span>
121
157
  </button>
122
158
  ))}
123
159
  </div>
124
160
  </>
161
+ ) : props.accounts ? (
162
+ <AccountForm signUpOpen={props.accounts.signUp} email={Boolean(props.accounts.email)} providers={props.accounts.providers ?? []} onSignIn={props.onSignIn} onSignUp={props.onSignUp} />
125
163
  ) : (
126
- <TokenSignIn />
164
+ <>
165
+ <h1>Sign in</h1>
166
+ <p className="muted">Sign-in isn't available for this app yet.</p>
167
+ </>
127
168
  )}
128
169
  </div>
129
170
  </div>
130
171
  );
131
172
  }
132
173
 
133
- /**
134
- * Production has no development sign-in. Until auth.authenticate in feltkit/app.ts verifies your
135
- * users with your identity provider, sign in with a session token created on the server.
136
- */
137
- function TokenSignIn() {
174
+ /** Sign in to an existing account, or create one (auth.accounts in feltkit/app.ts). */
175
+ function AccountForm(props: {
176
+ signUpOpen: boolean;
177
+ /** The app sends account e-mail (auth.accounts.email): offer "forgot password". */
178
+ email: boolean;
179
+ providers: Array<{ id: string; name: string }>;
180
+ onSignIn: (input: { email: string; password: string }) => Promise<void>;
181
+ onSignUp: (input: { email: string; password: string; name?: string }) => Promise<void>;
182
+ }) {
138
183
  const client = useFeltKit();
139
- const [token, setToken] = useState("");
140
- const submit = (e: FormEvent) => {
184
+ const [mode, setMode] = useState<"signin" | "signup" | "forgot">("signin");
185
+ const [name, setName] = useState("");
186
+ const [email, setEmail] = useState("");
187
+ const [password, setPassword] = useState("");
188
+ const [error, setError] = useState<string | null>(null);
189
+ const [info, setInfo] = useState<string | null>(null);
190
+ const [busy, setBusy] = useState(false);
191
+ const creating = mode === "signup";
192
+ const forgot = mode === "forgot";
193
+ const go = (next: typeof mode) => (setMode(next), setError(null), setInfo(null));
194
+ const submit = async (e: FormEvent) => {
141
195
  e.preventDefault();
142
- if (token.trim()) client.setToken(token.trim());
196
+ setError(null);
197
+ setInfo(null);
198
+ setBusy(true);
199
+ try {
200
+ if (forgot) setInfo((await client.requestPasswordReset(email)).message);
201
+ else if (creating) {
202
+ await props.onSignUp({ email, password, ...(name.trim() ? { name: name.trim() } : {}) });
203
+ if (!client.tokens.get()) setInfo("Check your e-mail: open the link we sent to verify your address, then sign in.");
204
+ } else await props.onSignIn({ email, password });
205
+ } catch (err) {
206
+ setError(describe(err));
207
+ } finally {
208
+ setBusy(false);
209
+ }
210
+ };
211
+ return (
212
+ <form className="stack" onSubmit={submit}>
213
+ <h1>{forgot ? "Reset your password" : creating ? "Create your account" : "Sign in"}</h1>
214
+ {!forgot &&
215
+ props.providers.map((p) => (
216
+ <button key={p.id} type="button" className="secondary wide" onClick={() => client.signInWith(p.id)}>
217
+ Continue with {p.name}
218
+ </button>
219
+ ))}
220
+ {!forgot && props.providers.length > 0 && <p className="muted small-text">or with your e-mail address</p>}
221
+ {creating && <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" aria-label="Name" autoComplete="name" />}
222
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="E-mail" aria-label="E-mail" autoComplete="email" required />
223
+ {!forgot && (
224
+ <input
225
+ type="password"
226
+ value={password}
227
+ onChange={(e) => setPassword(e.target.value)}
228
+ placeholder={creating ? "Password (12+ characters)" : "Password"}
229
+ aria-label="Password"
230
+ autoComplete={creating ? "new-password" : "current-password"}
231
+ minLength={creating ? 12 : undefined}
232
+ required
233
+ />
234
+ )}
235
+ {error && <p className="error-text">{error}</p>}
236
+ {info && <p className="muted">{info}</p>}
237
+ <button type="submit" disabled={busy || !email || (!forgot && !password)}>
238
+ {busy ? "One moment…" : forgot ? "Send reset link" : creating ? "Create account" : "Sign in"}
239
+ </button>
240
+ {mode === "signin" && props.email && (
241
+ <p className="muted small-text">
242
+ <button type="button" className="link" onClick={() => go("forgot")}>
243
+ Forgot your password?
244
+ </button>
245
+ </p>
246
+ )}
247
+ {(props.signUpOpen || forgot) && (
248
+ <p className="muted small-text">
249
+ {forgot ? "Remembered it? " : creating ? "Already have an account? " : "New here? "}
250
+ <button type="button" className="link" onClick={() => go(creating || forgot ? "signin" : "signup")}>
251
+ {creating || forgot ? "Sign in" : "Create an account"}
252
+ </button>
253
+ </p>
254
+ )}
255
+ </form>
256
+ );
257
+ }
258
+
259
+ /** A password reset link opened: choose a new password. */
260
+ function ResetForm({ token, onDone }: { token: string; onDone: () => void }) {
261
+ const client = useFeltKit();
262
+ const [password, setPassword] = useState("");
263
+ const [error, setError] = useState<string | null>(null);
264
+ const [busy, setBusy] = useState(false);
265
+ const submit = async (e: FormEvent) => {
266
+ e.preventDefault();
267
+ setError(null);
268
+ setBusy(true);
269
+ try {
270
+ await client.resetPassword(token, password);
271
+ onDone();
272
+ } catch (err) {
273
+ setError(describe(err));
274
+ } finally {
275
+ setBusy(false);
276
+ }
143
277
  };
144
278
  return (
145
279
  <form className="stack" onSubmit={submit}>
146
- <p className="muted">
147
- Sign in with a session token from whoever runs this deployment. If FeltKit deployed it, <code>npx feltkit vercel deploy</code> printed yours the first time; to make another, run
148
- this in your project's folder (it uses your deployment's FeltDB settings from <code>.env.local</code>):
149
- </p>
150
- <code className="block">npx feltkit session create --env production --type user --id you --role member</code>
151
- <p className="muted">If AppBoundry runs this app (from its <code>.app</code> artifact), its operator creates your user and issues your session instead:</p>
152
- <code className="block">appboundry admin sessions issue --user you --state &lt;installation&gt;</code>
153
- <input type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder="Session token (fks_…)" aria-label="Session token" autoComplete="off" />
154
- <button type="submit" disabled={!token.trim()}>
155
- Sign in
280
+ <h1>Choose a new password</h1>
281
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="New password (12+ characters)" aria-label="New password" autoComplete="new-password" minLength={12} required />
282
+ {error && <p className="error-text">{error}</p>}
283
+ <button type="submit" disabled={busy || !password}>
284
+ {busy ? "One moment…" : "Set password and sign in"}
156
285
  </button>
157
- <p className="muted small-text">To sign people in with your own login, add auth.authenticate in feltkit/app.ts.</p>
158
286
  </form>
159
287
  );
160
288
  }
@@ -127,3 +127,8 @@ code.block { display: block; padding: 8px 10px; border-radius: 6px; background:
127
127
  .identity, .identity select { max-width: 100%; min-width: 0; }
128
128
  .task-detail { padding-left: 16px; }
129
129
  }
130
+
131
+ /* Account sign-in (auth.accounts) */
132
+ button.link { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); text-decoration: underline; cursor: pointer; }
133
+ button.ghost.signout { font-size: 14px; }
134
+ .card form.stack input { padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
@@ -20,10 +20,10 @@
20
20
  "deploy:fly": "node scripts/deploy-fly.mjs"
21
21
  },
22
22
  "dependencies": {
23
- "@feltdb/feltkit": "0.3.8"
23
+ "@feltdb/feltkit": "0.3.10"
24
24
  },
25
25
  "devDependencies": {
26
- "@feltdb/core": "0.16.1",
26
+ "@feltdb/core": ">=0.17.1 <1.0.0",
27
27
  "@types/node": "22.19.1",
28
28
  "typescript": "5.9.3"
29
29
  }