@webtwizz/cloud 0.4.0 → 0.5.0
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/AI_GUIDE.md +63 -5
- package/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/dist/chunk-4PKQUQZD.js +6 -0
- package/dist/{chunk-G54JBUO3.js → chunk-WX3OU7ED.js} +17 -4
- package/dist/client.d.ts +13 -4
- package/dist/client.js +68 -770
- package/dist/db.js +1 -1
- package/dist/files.js +1 -1
- package/dist/index.js +1 -1
- package/dist/server.d.ts +29 -5
- package/dist/server.js +494 -1428
- package/package.json +7 -10
- package/dist/chunk-7EXXDRMF.js +0 -376
package/AI_GUIDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Read this before writing sign-ins, database code or file uploads on a site with WebTwizz Cloud. It matches the version installed next to it. Call all of it WebTwizz Cloud (sign-ins, database, files) and never name any company behind it.
|
|
4
4
|
|
|
5
|
-
Every setting is already in the site's environment (`WEBTWIZZ_AUTH_URL`, `WEBTWIZZ_AUTH_SECRET`, `DATABASE_URL`, `WEBTWIZZ_STORAGE_*`, `WEBTWIZZ_FILES_URL`). Never add, rename or print them.
|
|
5
|
+
Every setting is already in the site's environment (`WEBTWIZZ_AUTH_URL`, `WEBTWIZZ_AUTH_SECRET`, `DATABASE_URL`, `DATABASE_URL_UNPOOLED`, `WEBTWIZZ_STORAGE_*`, `WEBTWIZZ_FILES_URL`). Never add, rename or print them.
|
|
6
6
|
|
|
7
7
|
## Sign-ins
|
|
8
8
|
|
|
@@ -41,7 +41,15 @@ if (error) setMessage(authMessage(error));
|
|
|
41
41
|
|
|
42
42
|
For wording that fits the site, pass overrides: `authMessage(error, { USER_ALREADY_EXISTS: 'You already have a booking account.' })`.
|
|
43
43
|
|
|
44
|
-
When the owner turns on Confirm email, sign-up returns no session and the 6-digit code is emailed automatically. Show a code step: `authClient.emailOtp.verifyEmail({ email, otp })`, then sign them in, with a "Send a new code" button that calls `authClient.emailOtp.sendVerificationOtp({ email, type: 'email-verification' })`. Never send the first code yourself.
|
|
44
|
+
Confirm email is off unless the owner turns it on in Settings > Cloud > Users, so sign-up signs people straight in and an unconfirmed email never stops a sign-in. When the owner turns on Confirm email, sign-up returns no session and the 6-digit code is emailed automatically. Show a code step: `authClient.emailOtp.verifyEmail({ email, otp })`, then sign them in, with a "Send a new code" button that calls `authClient.emailOtp.sendVerificationOtp({ email, type: 'email-verification' })`. Never send the first code yourself.
|
|
45
|
+
|
|
46
|
+
Every sign-in form also handles `EMAIL_NOT_VERIFIED`, because the owner can turn Confirm email on at any time: members who joined while it was off then get that error, and a code is emailed to them at once. Keep what they typed, show `authMessage(error)` ("We sent you a code to confirm your email.") and the code field, then `authClient.emailOtp.verifyEmail({ email, otp })` and `authClient.signIn.email({ email, password })` again:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const { error } = await authClient.signIn.email({ email, password });
|
|
50
|
+
if (error?.code === 'EMAIL_NOT_VERIFIED') setNeedsCode(true);
|
|
51
|
+
if (error) setMessage(authMessage(error));
|
|
52
|
+
```
|
|
45
53
|
|
|
46
54
|
5. Protect pages in `src/proxy.ts`, listing only the protected routes in the matcher (a matcher over every path blocks the sign-in page's own scripts):
|
|
47
55
|
|
|
@@ -66,7 +74,35 @@ Never destructure `{ user }` from the top level.
|
|
|
66
74
|
|
|
67
75
|
8. Every sign-in page gets a "Forgot password?" link to `/reset-password`, a client page in two steps: their email, then `authClient.forgetPassword.emailOtp({ email })`, which emails a 6-digit code; then the code and a new password, then `authClient.emailOtp.resetPassword({ email, otp, password })`, then send them to sign in. Leave `/reset-password` out of the proxy matcher. The owner can also send a reset code from the Data panel, and it lands on this page.
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
9. Admins are accounts whose role is `'admin'`. Admin pages and admin writes check it on the server, the same way as step 6:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const { data: session } = await auth.getSession();
|
|
81
|
+
if (session?.user?.role !== 'admin') return Response.json({ error: 'Admins only' }, { status: 403 });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The first admin is the owner's own account on the site: they sign up like any visitor (their WebTwizz account is separate), then make that account an admin under Users in the Data panel, or ask you to do it. A new or removed role reaches their pages within about 10 seconds, and a blocked or deleted account is signed out within the same time.
|
|
85
|
+
|
|
86
|
+
Sign-ins are email and password, plus Google once the owner turns on Sign in with Google with their own Google app (Settings > Cloud > Users). Sign-in emails (reset codes, confirmations) are sent for the site automatically; never build email sending or add an email service for them. If the user asks for any other way to sign in, say WebTwizz Cloud sign-ins are email and password, and Google, for now.
|
|
87
|
+
|
|
88
|
+
When Google is on, add a "Continue with Google" button to the sign-in and sign-up forms:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const { error } = await authClient.signIn.social({ provider: 'google', callbackURL: '/account' });
|
|
92
|
+
if (error) setMessage(authMessage(error));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The visitor comes back to `callbackURL` signed in, also from inside the builder's preview, where Google opens in a popup. If it fails they come back to the page they started on with `?error=CODE` in the address: show `authMessage({ code })` for it. While Google is off the call returns `PROVIDER_NOT_FOUND`, so only show the button once the owner has turned it on.
|
|
96
|
+
|
|
97
|
+
Managing users from the site (creating accounts, changing roles, blocking) happens only in server code, a route handler or server action, after checking the signed-in user is an admin as in step 9. It works even when the owner has closed sign-ups:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const { data, error } = await auth.admin.createUser({ email, password, name, role: 'user' });
|
|
101
|
+
await auth.admin.setRole({ userId, role: 'admin' });
|
|
102
|
+
await auth.admin.banUser({ userId });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Never manage users from the browser: there `authClient.admin.*`, organizations, magic links and sign-in codes are switched off and return `NOT_FOUND`. Admin pages read `webtwizz.users` on the server, and the owner can also manage users under Users in the Data panel.
|
|
70
106
|
|
|
71
107
|
Sites built before this package import sign-ins from another package in `src/lib/auth/`. When you work on their sign-ins, move `server.ts`, `client.ts` and the auth route to the files above (same sign-ins, same users) and remove the old package.
|
|
72
108
|
|
|
@@ -96,12 +132,34 @@ import { createDb } from '@webtwizz/cloud/db';
|
|
|
96
132
|
export const sql = createDb();
|
|
97
133
|
```
|
|
98
134
|
|
|
99
|
-
It is the `postgres` package with pooler-safe settings, so queries look like ``await sql`select * from notes where user_id = ${session.user.id}` ``. Schema and data changes go through the `database` tool.
|
|
135
|
+
It is the `postgres` package (version 4) with pooler-safe settings, so queries look like ``await sql`select * from notes where user_id = ${session.user.id}` ``. Pass arrays straight in with a cast, ``where id = any(${ids}::int[])``; there is no `sql.array`. Schema and data changes go through the `database` tool.
|
|
136
|
+
|
|
137
|
+
`DATABASE_URL` goes through a connection pooler: each query may run on a connection another visitor just used. Never run a session-level `SET` (it leaks to other visitors, and a changed `search_path` breaks their queries) or `LISTEN`/`NOTIFY` (nothing arrives). Put settings inside a transaction with `SET LOCAL`, which ends with it:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
await sql.begin(async (tx) => {
|
|
141
|
+
await tx`set local statement_timeout = '5s'`;
|
|
142
|
+
await tx`update orders set status = 'paid' where id = ${id}`;
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Refresh changed data by polling (below) instead of `LISTEN`. Only a long-running script that truly needs one session, never a page or route, connects with `createDb({ url: process.env.DATABASE_URL_UNPOOLED })`.
|
|
100
147
|
|
|
101
148
|
The database can pause or fill up on the owner's plan. Wrap reads and saves in try/catch and show `MESSAGES.unavailable` on a page or `MESSAGES.saveFailed` on a form (`import { MESSAGES } from '@webtwizz/cloud'`). Visitors never see WebTwizz, plans or limits.
|
|
102
149
|
|
|
103
150
|
Pages that only show data set `export const revalidate = 60`, and the server action that saves calls `revalidatePath` for them. Pages for a signed-in user stay dynamic.
|
|
104
151
|
|
|
152
|
+
The database runs while pages read or save data and stops after 5 quiet minutes; its running time comes out of the owner's monthly allowance. So a page that refreshes data on a timer (a feed, a chat, a dashboard) refreshes only while the tab is visible, and again when the visitor comes back:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const refresh = () => { if (document.visibilityState === 'visible') load(); };
|
|
157
|
+
const timer = setInterval(refresh, 60_000);
|
|
158
|
+
document.addEventListener('visibilitychange', refresh);
|
|
159
|
+
return () => { clearInterval(timer); document.removeEventListener('visibilitychange', refresh); };
|
|
160
|
+
}, []);
|
|
161
|
+
```
|
|
162
|
+
|
|
105
163
|
## Files
|
|
106
164
|
|
|
107
165
|
Two buckets: `public` (anyone can view, for images shown on the site) and `private` (documents, opened through short-lived links). Images you place on pages yourself keep using the usual image tools; this is for what visitors or the site's features upload.
|
|
@@ -132,4 +190,4 @@ Show files with `createFiles().publicUrl(key)` in a plain `<img>` or link, or `a
|
|
|
132
190
|
|
|
133
191
|
## Owner tools
|
|
134
192
|
|
|
135
|
-
The owner manages users under Users in the Data panel (password reset, block, delete with their rows, export), sets who can sign up and the sign-in emails in Settings > Cloud > Users, and downloads everything from Settings > Cloud > Database. Point them there when they ask.
|
|
193
|
+
The owner manages users under Users in the Data panel (make admin, password reset, block, delete with their rows, export), sets who can sign up and the sign-in emails in Settings > Cloud > Users, and downloads everything from Settings > Cloud > Database. Point them there when they ask.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @webtwizz/cloud
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2838972: Sign-ins are now fully WebTwizz's own, with no code from the sign-in service's SDK in the package. Visitors who are signed in today sign in once more after a site moves to this version, because the cookies have new names.
|
|
8
|
+
|
|
9
|
+
- Sign-in forms always get `{ data, error }` back: a wrong password, a taken email, a bad code, a rate limit or a dropped connection no longer throws, so the form stops spinning and `authMessage` shows the right words. A site's own wording for `USER_ALREADY_EXISTS` also applies to the longer code the service sends.
|
|
10
|
+
- The browser client is Better Auth 1.7.14, matching the hosted service.
|
|
11
|
+
- Sign in with Google works when the owner has turned it on with their own Google app, read from the site's database. The visitor comes back through the site's own sign-in route, which finishes the sign-in and opens the page the site asked for. Inside the builder's preview, Google opens in a popup. Failures come back to the starting page with `?error=` and plain wording.
|
|
12
|
+
- Pages that check the session no longer fail with a server error when the session needs its daily renewal, or when the visitor was blocked or deleted: they render, and blocked or deleted visitors read as signed out.
|
|
13
|
+
- Visitors are no longer signed out 7 days after signing in. Renewed sessions reach the browser wherever the site can set cookies, and the session cookie lasts until the sign-in service ends the session.
|
|
14
|
+
- The cached session cookie stays small whatever is in a visitor's profile, and never includes the session token.
|
|
15
|
+
- Blocking, deleting or changing the role of a user takes effect within about 10 seconds instead of 5 minutes. If the sign-in service can't be reached, visitors stay signed in on their last known session for up to an hour.
|
|
16
|
+
- Cookies are named `__Host-webtwizz-auth.`, so other sites on webtwizz.app can't set or replace them. They carry no Domain, and cookies under any other name are ignored. Inside a frame, such as the builder's preview, they use `SameSite=None; Partitioned`; on plain-http localhost they drop the prefix and `Secure`, which Safari needs.
|
|
17
|
+
- The site's sign-in route refuses what browsers never need: the service's API reference and keys, admin powers such as signing in as another user or setting passwords, organizations, magic links, link emails, and sign-in codes, which could get around closed sign-ups. Refusals come back as errors `authMessage` words. The site's own server code can still manage users (`auth.admin.createUser`, `setRole` and the rest) after its own admin check; only the API reference, keys and tokens are closed to it.
|
|
18
|
+
- Every call to the sign-in service carries the visitor's IP address and browser.
|
|
19
|
+
- "Confirm your email" now reads "We sent you a code to confirm your email.", since the code goes out when an unconfirmed member signs in.
|
|
20
|
+
- The database client is `postgres` 4, which no longer crashes the server when the database stops in the middle of a transaction. Dates are still sent as before. `sql.array()` is gone: pass arrays directly.
|
|
21
|
+
- The guide covers the Google button, the code step on sign-in, `SET LOCAL` instead of session-level `SET`, and polling instead of `LISTEN` on the pooled database.
|
|
22
|
+
|
|
23
|
+
## 0.4.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 67343d5: Signed-in visitors can sign out, and `authClient` can read their session: since 0.2.0 the sign-in route answered both with a server error inside Next, because it copied a request Next hands over as a Proxy. Visitors who pressed Sign out stayed signed in.
|
|
28
|
+
|
|
3
29
|
## 0.4.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ WebTwizz puts every setting in the site's environment, so each module works with
|
|
|
13
13
|
| `@webtwizz/cloud` | `authMessage(error)` plain words for sign-in errors, `MESSAGES` for unavailable and failed saves |
|
|
14
14
|
| `@webtwizz/cloud/server` | `createAuth()` with `.handler()`, `.middleware()`, `.getSession()` and the server sign-in methods |
|
|
15
15
|
| `@webtwizz/cloud/client` | `createAuthClient()` for client components |
|
|
16
|
-
| `@webtwizz/cloud/db` | `createDb()`, a `postgres` client with pooler-safe settings |
|
|
16
|
+
| `@webtwizz/cloud/db` | `createDb()`, a `postgres` 4 client with pooler-safe settings |
|
|
17
17
|
| `@webtwizz/cloud/emails` | `handleSignInEmails()` for sites that send their own sign-in emails, `signInEmailCopy()` for the wording |
|
|
18
18
|
| `@webtwizz/cloud/files` | `createFiles()` with `uploadUrl`, `publicUrl`, `privateUrl`, `remove` |
|
|
19
19
|
|
|
@@ -38,7 +38,9 @@ npm run test:integration # real throwaway project, needs NEON_API_KEY
|
|
|
38
38
|
npm run build
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
`test/fixture` is a small Next app
|
|
41
|
+
`test/fixture` is a small Next app built the way WebTwizz builds sites (the same Next and React versions, `bun install`, `next build --webpack`, the guide's code), which CI builds against the packed tarball.
|
|
42
|
+
|
|
43
|
+
To try a change in the local WebTwizz preview site without publishing: `npm run preview` builds the package, puts it in the preview container's `node_modules` under a unique dev version, and restarts its dev server (about 10 seconds). `npm run dev` does the same on every save.
|
|
42
44
|
|
|
43
45
|
## Releases
|
|
44
46
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
2
|
var TRY_LATER = "Sign-in isn't working right now. Please try again in a minute.";
|
|
3
3
|
var TOO_MANY = "Too many tries. Wait a minute and try again.";
|
|
4
|
+
var GOOGLE_FAILED = "Signing in with Google didn't work. Please try again.";
|
|
4
5
|
var AUTH_MESSAGES = {
|
|
5
6
|
INVALID_EMAIL_OR_PASSWORD: "That email or password doesn't match.",
|
|
6
7
|
INVALID_PASSWORD: "That password doesn't match.",
|
|
@@ -10,7 +11,7 @@ var AUTH_MESSAGES = {
|
|
|
10
11
|
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "There's already an account with that email.",
|
|
11
12
|
PASSWORD_TOO_SHORT: "Use at least 8 characters for the password.",
|
|
12
13
|
PASSWORD_TOO_LONG: "That password is too long.",
|
|
13
|
-
EMAIL_NOT_VERIFIED: "
|
|
14
|
+
EMAIL_NOT_VERIFIED: "We sent you a code to confirm your email.",
|
|
14
15
|
EMAIL_ALREADY_VERIFIED: "Your email is already confirmed. You can sign in.",
|
|
15
16
|
EMAIL_MISMATCH: "That email doesn't match this account.",
|
|
16
17
|
EMAIL_CAN_NOT_BE_UPDATED: "The email on this account can't be changed.",
|
|
@@ -31,12 +32,24 @@ var AUTH_MESSAGES = {
|
|
|
31
32
|
LINKED_ACCOUNT_ALREADY_EXISTS: "That account is already connected.",
|
|
32
33
|
MISSING_FIELD: "Fill in every field and try again.",
|
|
33
34
|
VALIDATION_ERROR: "Check what you entered and try again.",
|
|
34
|
-
FIELD_NOT_ALLOWED: "Check what you entered and try again."
|
|
35
|
+
FIELD_NOT_ALLOWED: "Check what you entered and try again.",
|
|
36
|
+
NOT_FOUND: "That isn't available on this site.",
|
|
37
|
+
PROVIDER_NOT_FOUND: "This site doesn't offer that way to sign in.",
|
|
38
|
+
POPUP_BLOCKED: "Allow pop-ups for this site to sign in with Google.",
|
|
39
|
+
POPUP_CLOSED: "The sign-in window was closed before you finished.",
|
|
40
|
+
ACCESS_DENIED: "Signing in with Google was cancelled.",
|
|
41
|
+
SIGNUP_DISABLED: "New accounts can't be made on this site right now.",
|
|
42
|
+
ACCOUNT_NOT_LINKED: "That email already has an account. Sign in with your password.",
|
|
43
|
+
SIGN_IN_FAILED: GOOGLE_FAILED,
|
|
44
|
+
STATE_MISMATCH: GOOGLE_FAILED,
|
|
45
|
+
INVALID_CODE: GOOGLE_FAILED,
|
|
46
|
+
PLEASE_RESTART_THE_PROCESS: GOOGLE_FAILED
|
|
35
47
|
};
|
|
48
|
+
var ALIASES = { USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "USER_ALREADY_EXISTS", PROVIDER_NOT_SUPPORTED: "PROVIDER_NOT_FOUND" };
|
|
36
49
|
function authMessage(error, overrides = {}) {
|
|
37
50
|
if (error?.status === 429) return overrides.TOO_MANY_ATTEMPTS ?? TOO_MANY;
|
|
38
|
-
const code = error?.code;
|
|
39
|
-
return code && (overrides[code] ?? AUTH_MESSAGES[code]) || TRY_LATER;
|
|
51
|
+
const code = typeof error?.code === "string" ? error.code.toUpperCase() : void 0;
|
|
52
|
+
return code && (overrides[code] ?? overrides[ALIASES[code]] ?? AUTH_MESSAGES[code] ?? AUTH_MESSAGES[ALIASES[code]]) || TRY_LATER;
|
|
40
53
|
}
|
|
41
54
|
var MESSAGES = {
|
|
42
55
|
unavailable: "This isn't available right now. Please try again later.",
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { adminClient, emailOTPClient } from 'better-auth/client/plugins';
|
|
2
|
+
import { createAuthClient as createAuthClient$1 } from 'better-auth/react';
|
|
2
3
|
|
|
3
|
-
type
|
|
4
|
-
|
|
4
|
+
type AuthClientPlugins = Array<ReturnType<typeof adminClient<{}>> | ReturnType<typeof emailOTPClient>>;
|
|
5
|
+
type AuthClient = ReturnType<typeof createAuthClient$1<{
|
|
6
|
+
plugins: AuthClientPlugins;
|
|
7
|
+
}>>;
|
|
8
|
+
type AuthClientOptions = {
|
|
9
|
+
/** The site's auth route; only needed outside the browser. */
|
|
10
|
+
baseURL?: string;
|
|
11
|
+
};
|
|
12
|
+
/** Every call returns { data, error } and never throws, so a form can always show the error and stop spinning. */
|
|
13
|
+
declare function createAuthClient(options?: AuthClientOptions): AuthClient;
|
|
5
14
|
|
|
6
|
-
export { type AuthClient, createAuthClient };
|
|
15
|
+
export { type AuthClient, type AuthClientOptions, type AuthClientPlugins, createAuthClient };
|