@webtwizz/cloud 0.0.0-stage → 0.2.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 ADDED
@@ -0,0 +1,118 @@
1
+ # @webtwizz/cloud: building on WebTwizz Cloud
2
+
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
+
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.
6
+
7
+ ## Sign-ins
8
+
9
+ 1. `src/lib/auth/server.ts`
10
+
11
+ ```ts
12
+ import { createAuth } from '@webtwizz/cloud/server';
13
+
14
+ export const auth = createAuth();
15
+ ```
16
+
17
+ 2. `src/app/api/auth/[...path]/route.ts`
18
+
19
+ ```ts
20
+ import { auth } from '@/lib/auth/server';
21
+
22
+ export const { GET, POST, PUT, DELETE, PATCH } = auth.handler();
23
+ ```
24
+
25
+ 3. `src/lib/auth/client.ts`, the browser client. It takes no arguments and talks to the route above.
26
+
27
+ ```ts
28
+ import { createAuthClient } from '@webtwizz/cloud/client';
29
+
30
+ export const authClient = createAuthClient();
31
+ ```
32
+
33
+ 4. In client components: `authClient.signUp.email({ email, password, name })`, `authClient.signIn.email({ email, password })`, `authClient.signOut()`. Each returns `{ data, error }`. Never show `error.message`. Keep what they typed, `console.error(error)`, and show `authMessage(error)`:
34
+
35
+ ```ts
36
+ import { authMessage } from '@webtwizz/cloud';
37
+
38
+ const { error } = await authClient.signIn.email({ email, password });
39
+ if (error) setMessage(authMessage(error));
40
+ ```
41
+
42
+ For wording that fits the site, pass overrides: `authMessage(error, { USER_ALREADY_EXISTS: 'You already have a booking account.' })`.
43
+
44
+ 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):
45
+
46
+ ```ts
47
+ import { auth } from '@/lib/auth/server';
48
+
49
+ export default auth.middleware({ loginUrl: '/sign-in' });
50
+
51
+ export const config = { matcher: ['/account/:path*'] };
52
+ ```
53
+
54
+ 6. On the server, check the session before protected data or any write, in the route handler or server action itself:
55
+
56
+ ```ts
57
+ const { data: session } = await auth.getSession();
58
+ if (!session?.user) return Response.json({ error: 'Sign in first' }, { status: 401 });
59
+ ```
60
+
61
+ Never destructure `{ user }` from the top level.
62
+
63
+ 7. Signed-up users are rows in the `webtwizz.users` view (`id`, `name`, `email`, `email_verified`, `image`, `role`, `created_at`), kept by sign-ins. Read it, never write to it, never create a users or passwords table. Your own tables keep a plain `user_id uuid` column (no foreign key) set from `session.user.id`.
64
+
65
+ 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.
66
+
67
+ Sign-ins are email and password only. 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 another way to sign in, say WebTwizz Cloud sign-ins are email and password for now and build that.
68
+
69
+ 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.
70
+
71
+ ## Database
72
+
73
+ One shared client, server-side only (route handlers, server components, server actions):
74
+
75
+ ```ts
76
+ // src/lib/db.ts
77
+ import { createDb } from '@webtwizz/cloud/db';
78
+
79
+ export const sql = createDb();
80
+ ```
81
+
82
+ 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.
83
+
84
+ 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.
85
+
86
+ 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.
87
+
88
+ ## Files
89
+
90
+ 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.
91
+
92
+ ```ts
93
+ // src/app/api/uploads/route.ts
94
+ import { MESSAGES } from '@webtwizz/cloud';
95
+ import { FileError, createFiles } from '@webtwizz/cloud/files';
96
+ import { auth } from '@/lib/auth/server';
97
+
98
+ export async function POST(request: Request) {
99
+ const { data: session } = await auth.getSession();
100
+ if (!session?.user) return Response.json({ error: 'Sign in first' }, { status: 401 });
101
+ const { name, type, size } = await request.json();
102
+ try {
103
+ const upload = await createFiles().uploadUrl({ bucket: 'public', name, type, size, folder: session.user.id, accept: ['image/*'] });
104
+ return Response.json(upload);
105
+ } catch (error) {
106
+ if (error instanceof FileError) return Response.json({ error: error.message }, { status: 400 });
107
+ return Response.json({ error: MESSAGES.uploadsPaused }, { status: 503 });
108
+ }
109
+ }
110
+ ```
111
+
112
+ The browser then sends the file with exactly the returned headers: `fetch(upload.url, { method: 'PUT', headers: upload.headers, body: file })`. The link holds the file's size, so send the same file you described. If the PUT fails with an access error, storage is full: show `MESSAGES.uploadsPaused`. Save `upload.key` in your table.
113
+
114
+ Show files with `createFiles().publicUrl(key)` in a plain `<img>` or link, or `await createFiles().privateUrl(key)` (5 minutes; pass `{ download: 'name.pdf' }` to download) after checking the visitor may see it. Remove with `createFiles().remove(bucket, key)`. Never reuse a key.
115
+
116
+ ## Owner tools
117
+
118
+ 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @webtwizz/cloud
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 038e061: Visitors only see WebTwizz names: session cookies are `__Secure-webtwizz-auth.*`, the sign-in route drops the sign-in service's own response headers, sign-in log lines say `[webtwizz-auth]`, and uploads go through the site's files address like every other file link. Visitors signed in on 0.1.0 sign in again once.
8
+
9
+ ## 0.1.0
10
+
11
+ - Sign-ins (`createAuth`, `createAuthClient`), plain words for every sign-in error (`authMessage`), a pooler-safe database client (`createDb`) and file uploads and links (`createFiles`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebTwizz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # @webtwizz/cloud
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign-ins, database and file storage for sites built on [WebTwizz](https://webtwizz.com) Cloud.
4
+
5
+ ```sh
6
+ npm install @webtwizz/cloud
7
+ ```
8
+
9
+ WebTwizz puts every setting in the site's environment, so each module works with no arguments.
10
+
11
+ | Import | What it gives you |
12
+ | --- | --- |
13
+ | `@webtwizz/cloud` | `authMessage(error)` plain words for sign-in errors, `MESSAGES` for unavailable and failed saves |
14
+ | `@webtwizz/cloud/server` | `createAuth()` with `.handler()`, `.middleware()`, `.getSession()` and the server sign-in methods |
15
+ | `@webtwizz/cloud/client` | `createAuthClient()` for client components |
16
+ | `@webtwizz/cloud/db` | `createDb()`, a `postgres` client with pooler-safe settings |
17
+ | `@webtwizz/cloud/files` | `createFiles()` with `uploadUrl`, `publicUrl`, `privateUrl`, `remove` |
18
+
19
+ `server`, `db` and `files` are server-only. [AI_GUIDE.md](AI_GUIDE.md) has the full walkthrough and ships with each version.
20
+
21
+ ## Environment
22
+
23
+ | Variable | Used by |
24
+ | --- | --- |
25
+ | `WEBTWIZZ_AUTH_URL`, `WEBTWIZZ_AUTH_SECRET` | `server` |
26
+ | `DATABASE_URL` | `db` |
27
+ | `WEBTWIZZ_STORAGE_ENDPOINT`, `WEBTWIZZ_STORAGE_REGION`, `WEBTWIZZ_STORAGE_KEY`, `WEBTWIZZ_STORAGE_SECRET` | `files` |
28
+ | `WEBTWIZZ_STORAGE_READ_KEY`, `WEBTWIZZ_STORAGE_READ_SECRET` | `files` private links |
29
+ | `WEBTWIZZ_FILES_URL`, `WEBTWIZZ_STORAGE_MAX_FILE_MB` | `files`, optional |
30
+
31
+ ## Development
32
+
33
+ ```sh
34
+ npm ci
35
+ npm test # unit tests
36
+ npm run test:integration # real throwaway project, needs NEON_API_KEY
37
+ npm run build
38
+ ```
39
+
40
+ `test/fixture` is a small Next app that CI builds against the packed tarball.
41
+
42
+ ## Releases
43
+
44
+ - Add a changeset (`npx changeset`) to any PR that changes what sites get.
45
+ - When CI passes on `main`, the Release workflow versions, tags and publishes to npm with provenance (trusted publishing, no token).
46
+ - Dependency updates merged by Renovate ship as a patch on their own.
47
+ - Run the Release workflow by hand to publish the current commit under the `next` tag and try it on a test site first.
48
+
49
+ Sites pin an exact version, so a release never changes a live site until WebTwizz upgrades it.
50
+
51
+ The WebTwizz AI builds from its own copy of AI_GUIDE.md. After a release, run `npm run cloud:sync <version>` in the ai-service so new sites get that version and its guide.
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,376 @@
1
+ // node_modules/@neondatabase/auth/dist/better-auth-helpers-DlzEQzcv.mjs
2
+ import { AuthApiError, AuthError, isAuthApiError, isAuthError } from "@supabase/auth-js";
3
+ var SESSION_CACHE_TTL_MS = 6e4;
4
+ var CLOCK_SKEW_BUFFER_MS = 1e4;
5
+ var NEON_AUTH_SESSION_VERIFIER_PARAM_NAME = "neon_auth_session_verifier";
6
+ var NEON_AUTH_POPUP_PARAM_NAME = "neon_popup";
7
+ var NEON_AUTH_POPUP_CALLBACK_PARAM_NAME = "neon_popup_callback";
8
+ var NEON_AUTH_POPUP_CALLBACK_ROUTE = "/auth/callback";
9
+ var OAUTH_POPUP_MESSAGE_TYPE = "neon-auth:oauth-complete";
10
+ var AuthErrorCode = {
11
+ BadJwt: "bad_jwt",
12
+ InvalidCredentials: "invalid_credentials",
13
+ SessionExpired: "session_expired",
14
+ SessionNotFound: "session_not_found",
15
+ InvalidGrant: "invalid_grant",
16
+ UserNotFound: "user_not_found",
17
+ UserAlreadyExists: "user_already_exists",
18
+ EmailExists: "email_exists",
19
+ PhoneExists: "phone_exists",
20
+ EmailNotConfirmed: "email_not_confirmed",
21
+ PhoneNotConfirmed: "phone_not_confirmed",
22
+ ValidationFailed: "validation_failed",
23
+ BadJson: "bad_json",
24
+ WeakPassword: "weak_password",
25
+ EmailAddressInvalid: "email_address_invalid",
26
+ FeatureNotSupported: "feature_not_supported",
27
+ NotImplemented: "not_implemented",
28
+ OAuthProviderNotSupported: "oauth_provider_not_supported",
29
+ PhoneProviderDisabled: "phone_provider_disabled",
30
+ MagicLinkNotSupported: "magic_link_not_supported",
31
+ SsoProviderDisabled: "sso_provider_disabled",
32
+ AnonymousProviderDisabled: "anonymous_provider_disabled",
33
+ Web3ProviderDisabled: "web3_provider_disabled",
34
+ BadOAuthCallback: "bad_oauth_callback",
35
+ OAuthCallbackFailed: "oauth_callback_failed",
36
+ OverRequestRateLimit: "over_request_rate_limit",
37
+ OverEmailSendRateLimit: "over_email_send_rate_limit",
38
+ OverSmsSendRateLimit: "over_sms_send_rate_limit",
39
+ UnexpectedFailure: "unexpected_failure",
40
+ InternalError: "internal_error",
41
+ IdentityNotFound: "identity_not_found",
42
+ UnknownError: "unknown_error"
43
+ };
44
+ var ERROR_DEFINITIONS = {
45
+ [AuthErrorCode.BadJwt]: {
46
+ code: AuthErrorCode.BadJwt,
47
+ status: 401,
48
+ message: "Invalid or expired session token",
49
+ description: "The JWT token is malformed, expired, or has an invalid signature"
50
+ },
51
+ [AuthErrorCode.InvalidCredentials]: {
52
+ code: AuthErrorCode.InvalidCredentials,
53
+ status: 401,
54
+ message: "Invalid email or password",
55
+ description: "The provided credentials do not match any user account"
56
+ },
57
+ [AuthErrorCode.SessionExpired]: {
58
+ code: AuthErrorCode.SessionExpired,
59
+ status: 401,
60
+ message: "Session has expired",
61
+ description: "The user session has exceeded its timeout period"
62
+ },
63
+ [AuthErrorCode.SessionNotFound]: {
64
+ code: AuthErrorCode.SessionNotFound,
65
+ status: 401,
66
+ message: "No active session found",
67
+ description: "The user does not have an active session or the session was invalidated"
68
+ },
69
+ [AuthErrorCode.InvalidGrant]: {
70
+ code: AuthErrorCode.InvalidGrant,
71
+ status: 401,
72
+ message: "Invalid authorization grant",
73
+ description: "OAuth/OIDC grant validation failed"
74
+ },
75
+ [AuthErrorCode.UserNotFound]: {
76
+ code: AuthErrorCode.UserNotFound,
77
+ status: 404,
78
+ message: "User not found",
79
+ description: "No user exists with the provided identifier"
80
+ },
81
+ [AuthErrorCode.UserAlreadyExists]: {
82
+ code: AuthErrorCode.UserAlreadyExists,
83
+ status: 409,
84
+ message: "User already exists",
85
+ description: "A user with this email or phone number is already registered"
86
+ },
87
+ [AuthErrorCode.EmailExists]: {
88
+ code: AuthErrorCode.EmailExists,
89
+ status: 409,
90
+ message: "Email address already registered",
91
+ description: "This email address is already associated with an account"
92
+ },
93
+ [AuthErrorCode.PhoneExists]: {
94
+ code: AuthErrorCode.PhoneExists,
95
+ status: 409,
96
+ message: "Phone number already registered",
97
+ description: "This phone number is already associated with an account"
98
+ },
99
+ [AuthErrorCode.EmailNotConfirmed]: {
100
+ code: AuthErrorCode.EmailNotConfirmed,
101
+ status: 422,
102
+ message: "Email verification required",
103
+ description: "The user must verify their email before signing in"
104
+ },
105
+ [AuthErrorCode.PhoneNotConfirmed]: {
106
+ code: AuthErrorCode.PhoneNotConfirmed,
107
+ status: 422,
108
+ message: "Phone verification required",
109
+ description: "The user must verify their phone number before signing in"
110
+ },
111
+ [AuthErrorCode.ValidationFailed]: {
112
+ code: AuthErrorCode.ValidationFailed,
113
+ status: 400,
114
+ message: "Invalid request parameters",
115
+ description: "One or more request parameters are invalid or missing"
116
+ },
117
+ [AuthErrorCode.BadJson]: {
118
+ code: AuthErrorCode.BadJson,
119
+ status: 400,
120
+ message: "Invalid JSON in request body",
121
+ description: "The request body contains malformed JSON"
122
+ },
123
+ [AuthErrorCode.WeakPassword]: {
124
+ code: AuthErrorCode.WeakPassword,
125
+ status: 400,
126
+ message: "Password does not meet security requirements",
127
+ description: "The password is too weak or does not meet complexity requirements"
128
+ },
129
+ [AuthErrorCode.EmailAddressInvalid]: {
130
+ code: AuthErrorCode.EmailAddressInvalid,
131
+ status: 400,
132
+ message: "Invalid email address format",
133
+ description: "The provided email address is not in a valid format"
134
+ },
135
+ [AuthErrorCode.FeatureNotSupported]: {
136
+ code: AuthErrorCode.FeatureNotSupported,
137
+ status: 403,
138
+ message: "Feature not available",
139
+ description: "This feature is not supported in the current configuration"
140
+ },
141
+ [AuthErrorCode.NotImplemented]: {
142
+ code: AuthErrorCode.NotImplemented,
143
+ status: 501,
144
+ message: "Feature not implemented",
145
+ description: "This feature has not been implemented yet"
146
+ },
147
+ [AuthErrorCode.OAuthProviderNotSupported]: {
148
+ code: AuthErrorCode.OAuthProviderNotSupported,
149
+ status: 403,
150
+ message: "OAuth provider not supported",
151
+ description: "The requested OAuth provider is not enabled"
152
+ },
153
+ [AuthErrorCode.PhoneProviderDisabled]: {
154
+ code: AuthErrorCode.PhoneProviderDisabled,
155
+ status: 403,
156
+ message: "Phone authentication not available",
157
+ description: "Phone number authentication is not enabled"
158
+ },
159
+ [AuthErrorCode.MagicLinkNotSupported]: {
160
+ code: AuthErrorCode.MagicLinkNotSupported,
161
+ status: 403,
162
+ message: "Magic link authentication not available",
163
+ description: "Magic link authentication is not supported"
164
+ },
165
+ [AuthErrorCode.SsoProviderDisabled]: {
166
+ code: AuthErrorCode.SsoProviderDisabled,
167
+ status: 403,
168
+ message: "SSO not supported",
169
+ description: "Enterprise SSO authentication is not available"
170
+ },
171
+ [AuthErrorCode.AnonymousProviderDisabled]: {
172
+ code: AuthErrorCode.AnonymousProviderDisabled,
173
+ status: 403,
174
+ message: "Anonymous authentication not available",
175
+ description: "Anonymous sign-in is not enabled"
176
+ },
177
+ [AuthErrorCode.Web3ProviderDisabled]: {
178
+ code: AuthErrorCode.Web3ProviderDisabled,
179
+ status: 403,
180
+ message: "Web3 authentication not supported",
181
+ description: "Web3/blockchain authentication is not available"
182
+ },
183
+ [AuthErrorCode.BadOAuthCallback]: {
184
+ code: AuthErrorCode.BadOAuthCallback,
185
+ status: 400,
186
+ message: "Invalid OAuth callback",
187
+ description: "The OAuth callback request is missing required parameters"
188
+ },
189
+ [AuthErrorCode.OAuthCallbackFailed]: {
190
+ code: AuthErrorCode.OAuthCallbackFailed,
191
+ status: 500,
192
+ message: "OAuth authentication failed",
193
+ description: "The OAuth callback completed but no session was created"
194
+ },
195
+ [AuthErrorCode.OverRequestRateLimit]: {
196
+ code: AuthErrorCode.OverRequestRateLimit,
197
+ status: 429,
198
+ message: "Too many requests",
199
+ description: "Rate limit exceeded. Please try again later"
200
+ },
201
+ [AuthErrorCode.OverEmailSendRateLimit]: {
202
+ code: AuthErrorCode.OverEmailSendRateLimit,
203
+ status: 429,
204
+ message: "Too many email requests",
205
+ description: "Too many emails sent. Please wait before trying again"
206
+ },
207
+ [AuthErrorCode.OverSmsSendRateLimit]: {
208
+ code: AuthErrorCode.OverSmsSendRateLimit,
209
+ status: 429,
210
+ message: "Too many SMS requests",
211
+ description: "Too many SMS messages sent. Please wait before trying again"
212
+ },
213
+ [AuthErrorCode.UnexpectedFailure]: {
214
+ code: AuthErrorCode.UnexpectedFailure,
215
+ status: 500,
216
+ message: "An unexpected error occurred",
217
+ description: "The server encountered an unexpected condition"
218
+ },
219
+ [AuthErrorCode.InternalError]: {
220
+ code: AuthErrorCode.InternalError,
221
+ status: 500,
222
+ message: "Internal server error",
223
+ description: "An internal error occurred while processing the request"
224
+ },
225
+ [AuthErrorCode.IdentityNotFound]: {
226
+ code: AuthErrorCode.IdentityNotFound,
227
+ status: 404,
228
+ message: "Identity not found",
229
+ description: "The requested user identity does not exist"
230
+ },
231
+ [AuthErrorCode.UnknownError]: {
232
+ code: AuthErrorCode.UnknownError,
233
+ status: 500,
234
+ message: "An unknown error occurred",
235
+ description: "The error could not be categorized"
236
+ }
237
+ };
238
+ function getErrorDefinition(code) {
239
+ return ERROR_DEFINITIONS[code];
240
+ }
241
+ var BETTER_AUTH_ERROR_MAP = {
242
+ "INVALID_EMAIL_OR_PASSWORD": AuthErrorCode.InvalidCredentials,
243
+ "INVALID_PASSWORD": AuthErrorCode.InvalidCredentials,
244
+ "INVALID_EMAIL": AuthErrorCode.EmailAddressInvalid,
245
+ "USER_NOT_FOUND": AuthErrorCode.UserNotFound,
246
+ "INVALID_TOKEN": AuthErrorCode.BadJwt,
247
+ "SESSION_EXPIRED": AuthErrorCode.SessionExpired,
248
+ "FAILED_TO_GET_SESSION": AuthErrorCode.SessionNotFound,
249
+ "USER_ALREADY_EXISTS": AuthErrorCode.UserAlreadyExists,
250
+ "EMAIL_NOT_VERIFIED": AuthErrorCode.EmailNotConfirmed,
251
+ "USER_EMAIL_NOT_FOUND": AuthErrorCode.UserNotFound,
252
+ "PASSWORD_TOO_SHORT": AuthErrorCode.WeakPassword,
253
+ "PASSWORD_TOO_LONG": AuthErrorCode.WeakPassword,
254
+ "USER_ALREADY_HAS_PASSWORD": AuthErrorCode.ValidationFailed,
255
+ "CREDENTIAL_ACCOUNT_NOT_FOUND": AuthErrorCode.IdentityNotFound,
256
+ "FAILED_TO_UNLINK_LAST_ACCOUNT": AuthErrorCode.ValidationFailed,
257
+ "ACCOUNT_NOT_FOUND": AuthErrorCode.IdentityNotFound,
258
+ "SOCIAL_ACCOUNT_ALREADY_LINKED": AuthErrorCode.ValidationFailed,
259
+ "PROVIDER_NOT_FOUND": AuthErrorCode.OAuthProviderNotSupported,
260
+ "ID_TOKEN_NOT_SUPPORTED": AuthErrorCode.FeatureNotSupported,
261
+ "FAILED_TO_CREATE_USER": AuthErrorCode.InternalError,
262
+ "FAILED_TO_CREATE_SESSION": AuthErrorCode.InternalError,
263
+ "FAILED_TO_UPDATE_USER": AuthErrorCode.InternalError,
264
+ "EMAIL_CAN_NOT_BE_UPDATED": AuthErrorCode.FeatureNotSupported
265
+ };
266
+ var STATUS_CODE_ERROR_MAP = {
267
+ 400: AuthErrorCode.ValidationFailed,
268
+ 401: AuthErrorCode.BadJwt,
269
+ 403: AuthErrorCode.FeatureNotSupported,
270
+ 404: AuthErrorCode.UserNotFound,
271
+ 409: AuthErrorCode.UserAlreadyExists,
272
+ 422: AuthErrorCode.ValidationFailed,
273
+ 429: AuthErrorCode.OverRequestRateLimit,
274
+ 500: AuthErrorCode.UnexpectedFailure,
275
+ 501: AuthErrorCode.NotImplemented,
276
+ 503: AuthErrorCode.FeatureNotSupported
277
+ };
278
+ function normalizeBetterAuthError(error) {
279
+ if (error !== null && error !== void 0 && typeof error === "object" && "status" in error && "statusText" in error) {
280
+ const betterError = error;
281
+ const status = betterError.status;
282
+ if ("code" in betterError && betterError.code && typeof betterError.code === "string") {
283
+ const mappedCode = BETTER_AUTH_ERROR_MAP[betterError.code];
284
+ if (mappedCode) {
285
+ const def$2 = getErrorDefinition(mappedCode);
286
+ return createNormalizedError(def$2.message, def$2.status, def$2.code, status);
287
+ }
288
+ }
289
+ const def$1 = getErrorDefinition(mapStatusCodeToErrorCode(status, betterError.message || betterError.statusText));
290
+ return createNormalizedError(betterError.message || def$1.message, status, def$1.code, status);
291
+ }
292
+ if (error instanceof Error) {
293
+ const def$1 = getErrorDefinition(mapMessageToErrorCode(error.message));
294
+ return createNormalizedError(error.message || def$1.message, def$1.status, def$1.code, def$1.status);
295
+ }
296
+ const def = getErrorDefinition(AuthErrorCode.UnknownError);
297
+ return new AuthError(def.message, def.status, def.code);
298
+ }
299
+ function mapStatusCodeToErrorCode(status, message) {
300
+ const lowerMessage = message?.toLowerCase() || "";
301
+ switch (status) {
302
+ case 401:
303
+ if (lowerMessage.includes("token") || lowerMessage.includes("jwt")) return AuthErrorCode.BadJwt;
304
+ if (lowerMessage.includes("session")) return AuthErrorCode.SessionNotFound;
305
+ if (lowerMessage.includes("expired")) return AuthErrorCode.SessionExpired;
306
+ return AuthErrorCode.InvalidCredentials;
307
+ case 404:
308
+ if (lowerMessage.includes("identity") || lowerMessage.includes("account")) return AuthErrorCode.IdentityNotFound;
309
+ if (lowerMessage.includes("session")) return AuthErrorCode.SessionNotFound;
310
+ return AuthErrorCode.UserNotFound;
311
+ case 409:
312
+ if (lowerMessage.includes("email")) return AuthErrorCode.EmailExists;
313
+ if (lowerMessage.includes("phone")) return AuthErrorCode.PhoneExists;
314
+ return AuthErrorCode.UserAlreadyExists;
315
+ case 422:
316
+ if (lowerMessage.includes("email") && lowerMessage.includes("confirm")) return AuthErrorCode.EmailNotConfirmed;
317
+ if (lowerMessage.includes("phone") && lowerMessage.includes("confirm")) return AuthErrorCode.PhoneNotConfirmed;
318
+ return AuthErrorCode.ValidationFailed;
319
+ case 429:
320
+ if (lowerMessage.includes("email")) return AuthErrorCode.OverEmailSendRateLimit;
321
+ if (lowerMessage.includes("sms") || lowerMessage.includes("phone")) return AuthErrorCode.OverSmsSendRateLimit;
322
+ return AuthErrorCode.OverRequestRateLimit;
323
+ case 400:
324
+ if (lowerMessage.includes("password") && lowerMessage.includes("weak")) return AuthErrorCode.WeakPassword;
325
+ if (lowerMessage.includes("email") && lowerMessage.includes("invalid")) return AuthErrorCode.EmailAddressInvalid;
326
+ if (lowerMessage.includes("json")) return AuthErrorCode.BadJson;
327
+ if (lowerMessage.includes("oauth") || lowerMessage.includes("callback")) return AuthErrorCode.BadOAuthCallback;
328
+ return AuthErrorCode.ValidationFailed;
329
+ case 403:
330
+ if (lowerMessage.includes("provider") || lowerMessage.includes("oauth")) return AuthErrorCode.OAuthProviderNotSupported;
331
+ if (lowerMessage.includes("phone")) return AuthErrorCode.PhoneProviderDisabled;
332
+ if (lowerMessage.includes("sso")) return AuthErrorCode.SsoProviderDisabled;
333
+ return AuthErrorCode.FeatureNotSupported;
334
+ case 501:
335
+ return AuthErrorCode.NotImplemented;
336
+ case 503:
337
+ return AuthErrorCode.FeatureNotSupported;
338
+ default:
339
+ if (lowerMessage.includes("oauth")) return AuthErrorCode.OAuthCallbackFailed;
340
+ return AuthErrorCode.UnexpectedFailure;
341
+ }
342
+ }
343
+ function mapMessageToErrorCode(message) {
344
+ const lower = message.toLowerCase();
345
+ if (lower.includes("invalid login") || lower.includes("incorrect") || lower.includes("wrong password")) return AuthErrorCode.InvalidCredentials;
346
+ if (lower.includes("token") && (lower.includes("invalid") || lower.includes("expired"))) return AuthErrorCode.BadJwt;
347
+ if (lower.includes("session") && lower.includes("expired")) return AuthErrorCode.SessionExpired;
348
+ if (lower.includes("session") && lower.includes("not found")) return AuthErrorCode.SessionNotFound;
349
+ if (lower.includes("already exists") || lower.includes("already registered")) return AuthErrorCode.UserAlreadyExists;
350
+ if (lower.includes("not found") && lower.includes("user")) return AuthErrorCode.UserNotFound;
351
+ if (lower.includes("not found") && lower.includes("identity")) return AuthErrorCode.IdentityNotFound;
352
+ if (lower.includes("email") && lower.includes("not confirmed")) return AuthErrorCode.EmailNotConfirmed;
353
+ if (lower.includes("phone") && lower.includes("not confirmed")) return AuthErrorCode.PhoneNotConfirmed;
354
+ if (lower.includes("weak password") || lower.includes("password") && lower.includes("requirements")) return AuthErrorCode.WeakPassword;
355
+ if (lower.includes("email") && lower.includes("invalid")) return AuthErrorCode.EmailAddressInvalid;
356
+ if (lower.includes("rate limit") || lower.includes("too many requests")) return AuthErrorCode.OverRequestRateLimit;
357
+ if (lower.includes("oauth") && lower.includes("failed")) return AuthErrorCode.OAuthCallbackFailed;
358
+ if (lower.includes("provider") && lower.includes("not supported")) return AuthErrorCode.OAuthProviderNotSupported;
359
+ return AuthErrorCode.UnexpectedFailure;
360
+ }
361
+ function createNormalizedError(message, targetStatus, code, _originalStatus) {
362
+ const status = targetStatus;
363
+ if (status !== 500 && status !== 501 && status !== 503) return new AuthApiError(message, status, code);
364
+ return new AuthError(message, status, code);
365
+ }
366
+
367
+ export {
368
+ SESSION_CACHE_TTL_MS,
369
+ CLOCK_SKEW_BUFFER_MS,
370
+ NEON_AUTH_SESSION_VERIFIER_PARAM_NAME,
371
+ NEON_AUTH_POPUP_PARAM_NAME,
372
+ NEON_AUTH_POPUP_CALLBACK_PARAM_NAME,
373
+ NEON_AUTH_POPUP_CALLBACK_ROUTE,
374
+ OAUTH_POPUP_MESSAGE_TYPE,
375
+ normalizeBetterAuthError
376
+ };
@@ -0,0 +1,12 @@
1
+ // src/env.ts
2
+ function env(name) {
3
+ const value = process.env[name];
4
+ if (!value) {
5
+ throw new Error(`${name} is not set. WebTwizz sets it on sites that have WebTwizz Cloud (Settings > Cloud).`);
6
+ }
7
+ return value;
8
+ }
9
+
10
+ export {
11
+ env
12
+ };
@@ -0,0 +1,53 @@
1
+ // src/errors.ts
2
+ var TRY_LATER = "Sign-in isn't working right now. Please try again in a minute.";
3
+ var TOO_MANY = "Too many tries. Wait a minute and try again.";
4
+ var AUTH_MESSAGES = {
5
+ INVALID_EMAIL_OR_PASSWORD: "That email or password doesn't match.",
6
+ INVALID_PASSWORD: "That password doesn't match.",
7
+ INVALID_EMAIL: "That email doesn't look right.",
8
+ USER_NOT_FOUND: "There's no account with that email.",
9
+ USER_ALREADY_EXISTS: "There's already an account with that email.",
10
+ USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "There's already an account with that email.",
11
+ PASSWORD_TOO_SHORT: "Use at least 8 characters for the password.",
12
+ PASSWORD_TOO_LONG: "That password is too long.",
13
+ EMAIL_NOT_VERIFIED: "Confirm your email first: check your inbox for the code.",
14
+ EMAIL_ALREADY_VERIFIED: "Your email is already confirmed. You can sign in.",
15
+ EMAIL_MISMATCH: "That email doesn't match this account.",
16
+ EMAIL_CAN_NOT_BE_UPDATED: "The email on this account can't be changed.",
17
+ CHANGE_EMAIL_DISABLED: "The email on this account can't be changed.",
18
+ INVALID_OTP: "That code isn't right. Check it and try again.",
19
+ OTP_EXPIRED: "That code has expired. Ask for a new one.",
20
+ INVALID_TOKEN: "That link isn't valid anymore. Ask for a new one.",
21
+ TOKEN_EXPIRED: "That link has expired. Ask for a new one.",
22
+ TOO_MANY_ATTEMPTS: TOO_MANY,
23
+ BANNED_USER: "This account can't sign in.",
24
+ SESSION_EXPIRED: "You were signed out. Please sign in again.",
25
+ SESSION_NOT_FRESH: "For your security, sign in again to do this.",
26
+ CREDENTIAL_ACCOUNT_NOT_FOUND: "This account doesn't have a password yet.",
27
+ USER_ALREADY_HAS_PASSWORD: "This account already has a password.",
28
+ PASSWORD_ALREADY_SET: "This account already has a password.",
29
+ FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't remove your only way to sign in.",
30
+ SOCIAL_ACCOUNT_ALREADY_LINKED: "That account is already connected.",
31
+ LINKED_ACCOUNT_ALREADY_EXISTS: "That account is already connected.",
32
+ MISSING_FIELD: "Fill in every field and try again.",
33
+ VALIDATION_ERROR: "Check what you entered and try again.",
34
+ FIELD_NOT_ALLOWED: "Check what you entered and try again."
35
+ };
36
+ function authMessage(error, overrides = {}) {
37
+ 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;
40
+ }
41
+ var MESSAGES = {
42
+ unavailable: "This isn't available right now. Please try again later.",
43
+ saveFailed: "Couldn't save right now. Please try again later.",
44
+ uploadsPaused: "Uploads are paused right now. Please try again later.",
45
+ fileType: "That type of file can't be uploaded here.",
46
+ fileTooLarge: (mb) => `That file is too big. The limit is ${mb} MB.`
47
+ };
48
+
49
+ export {
50
+ AUTH_MESSAGES,
51
+ authMessage,
52
+ MESSAGES
53
+ };
@@ -0,0 +1,6 @@
1
+ import { createAuthClient as createAuthClient$1 } from '@neondatabase/auth/next';
2
+
3
+ type AuthClient = ReturnType<typeof createAuthClient$1>;
4
+ declare function createAuthClient(): AuthClient;
5
+
6
+ export { type AuthClient, createAuthClient };