@webtwizz/cloud 0.0.0-stage → 0.1.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 +118 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/chunk-ESAV545E.js +12 -0
- package/dist/chunk-G54JBUO3.js +53 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +8 -0
- package/dist/db.d.ts +9 -0
- package/dist/db.js +13 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +92 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +19 -0
- package/package.json +84 -4
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
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,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webtwizz/cloud
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -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
|
+
};
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
2
|
+
|
|
3
|
+
type DbOptions = postgres.Options<Record<string, postgres.PostgresType>> & {
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
type Db = postgres.Sql<Record<string, never>>;
|
|
7
|
+
declare function createDb({ url, ...options }?: DbOptions): Db;
|
|
8
|
+
|
|
9
|
+
export { type Db, type DbOptions, createDb };
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
env
|
|
3
|
+
} from "./chunk-ESAV545E.js";
|
|
4
|
+
|
|
5
|
+
// src/db.ts
|
|
6
|
+
import "server-only";
|
|
7
|
+
import postgres from "postgres";
|
|
8
|
+
function createDb({ url, ...options } = {}) {
|
|
9
|
+
return postgres(url ?? env("DATABASE_URL"), { ...options, prepare: false });
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
createDb
|
|
13
|
+
};
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type Bucket = 'public' | 'private';
|
|
2
|
+
type UploadRequest = {
|
|
3
|
+
bucket: Bucket;
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
size: number;
|
|
7
|
+
folder?: string;
|
|
8
|
+
accept?: string[];
|
|
9
|
+
};
|
|
10
|
+
type Upload = {
|
|
11
|
+
url: string;
|
|
12
|
+
key: string;
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
declare class FileError extends Error {
|
|
16
|
+
code: 'too_large' | 'type';
|
|
17
|
+
constructor(code: 'too_large' | 'type', message: string);
|
|
18
|
+
}
|
|
19
|
+
declare function safeName(name: string): string;
|
|
20
|
+
declare function accepts(type: string, accept: string[]): boolean;
|
|
21
|
+
declare function createFiles(): {
|
|
22
|
+
maxFileMb: number;
|
|
23
|
+
/** A 5-minute link the browser PUTs the file to, with the exact headers it must send. */
|
|
24
|
+
uploadUrl({ bucket, name, type, size, folder, accept }: UploadRequest): Promise<Upload>;
|
|
25
|
+
publicUrl(key: string): string;
|
|
26
|
+
/** A short-lived link to a private file; check the visitor may see it first. */
|
|
27
|
+
privateUrl(key: string, { expiresIn, download }?: {
|
|
28
|
+
expiresIn?: number;
|
|
29
|
+
download?: string;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
remove(bucket: Bucket, key: string): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
type Files = ReturnType<typeof createFiles>;
|
|
34
|
+
|
|
35
|
+
export { type Bucket, FileError, type Files, type Upload, type UploadRequest, accepts, createFiles, safeName };
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MESSAGES
|
|
3
|
+
} from "./chunk-G54JBUO3.js";
|
|
4
|
+
import {
|
|
5
|
+
env
|
|
6
|
+
} from "./chunk-ESAV545E.js";
|
|
7
|
+
|
|
8
|
+
// src/files.ts
|
|
9
|
+
import "server-only";
|
|
10
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
11
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
12
|
+
var FileError = class extends Error {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "FileError";
|
|
17
|
+
}
|
|
18
|
+
code;
|
|
19
|
+
};
|
|
20
|
+
var IMMUTABLE = "public, max-age=31536000, immutable";
|
|
21
|
+
function safeName(name) {
|
|
22
|
+
const cleaned = name.normalize("NFKD").replace(/[^\w.-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|-+$/g, "");
|
|
23
|
+
return cleaned.slice(-80) || "file";
|
|
24
|
+
}
|
|
25
|
+
function accepts(type, accept) {
|
|
26
|
+
return accept.some((rule) => rule.endsWith("/*") ? type.startsWith(rule.slice(0, -1)) : type === rule);
|
|
27
|
+
}
|
|
28
|
+
function encodeKey(key) {
|
|
29
|
+
return key.split("/").map(encodeURIComponent).join("/");
|
|
30
|
+
}
|
|
31
|
+
function createFiles() {
|
|
32
|
+
const endpoint = env("WEBTWIZZ_STORAGE_ENDPOINT").replace(/\/+$/, "");
|
|
33
|
+
const filesUrl = (process.env.WEBTWIZZ_FILES_URL || endpoint).replace(/\/+$/, "");
|
|
34
|
+
const region = env("WEBTWIZZ_STORAGE_REGION");
|
|
35
|
+
const client = (key, secret) => new S3Client({
|
|
36
|
+
region,
|
|
37
|
+
endpoint,
|
|
38
|
+
forcePathStyle: true,
|
|
39
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
40
|
+
credentials: { accessKeyId: env(key), secretAccessKey: env(secret) }
|
|
41
|
+
});
|
|
42
|
+
const writer = client("WEBTWIZZ_STORAGE_KEY", "WEBTWIZZ_STORAGE_SECRET");
|
|
43
|
+
let reader;
|
|
44
|
+
const maxFileMb = Number(process.env.WEBTWIZZ_STORAGE_MAX_FILE_MB) || 0;
|
|
45
|
+
return {
|
|
46
|
+
maxFileMb,
|
|
47
|
+
/** A 5-minute link the browser PUTs the file to, with the exact headers it must send. */
|
|
48
|
+
async uploadUrl({ bucket, name, type, size, folder, accept }) {
|
|
49
|
+
if (maxFileMb && size > maxFileMb * 1024 * 1024) throw new FileError("too_large", MESSAGES.fileTooLarge(maxFileMb));
|
|
50
|
+
if (accept && !accepts(type, accept)) throw new FileError("type", MESSAGES.fileType);
|
|
51
|
+
const prefix = folder ? `${folder.split("/").map(safeName).join("/")}/` : "";
|
|
52
|
+
const key = `${prefix}${crypto.randomUUID()}-${safeName(name)}`;
|
|
53
|
+
const headers = { "Content-Type": type };
|
|
54
|
+
if (bucket === "public") headers["Cache-Control"] = IMMUTABLE;
|
|
55
|
+
const command = new PutObjectCommand({
|
|
56
|
+
Bucket: bucket,
|
|
57
|
+
Key: key,
|
|
58
|
+
ContentType: type,
|
|
59
|
+
ContentLength: size,
|
|
60
|
+
CacheControl: headers["Cache-Control"]
|
|
61
|
+
});
|
|
62
|
+
const url = await getSignedUrl(writer, command, {
|
|
63
|
+
expiresIn: 300,
|
|
64
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-type", "content-length"])
|
|
65
|
+
});
|
|
66
|
+
return { url, key, headers };
|
|
67
|
+
},
|
|
68
|
+
publicUrl(key) {
|
|
69
|
+
return `${filesUrl}/public/${encodeKey(key)}`;
|
|
70
|
+
},
|
|
71
|
+
/** A short-lived link to a private file; check the visitor may see it first. */
|
|
72
|
+
async privateUrl(key, { expiresIn = 300, download } = {}) {
|
|
73
|
+
reader ??= client("WEBTWIZZ_STORAGE_READ_KEY", "WEBTWIZZ_STORAGE_READ_SECRET");
|
|
74
|
+
const command = new GetObjectCommand({
|
|
75
|
+
Bucket: "private",
|
|
76
|
+
Key: key,
|
|
77
|
+
ResponseContentDisposition: download ? `attachment; filename="${safeName(download)}"` : void 0
|
|
78
|
+
});
|
|
79
|
+
const url = await getSignedUrl(reader, command, { expiresIn });
|
|
80
|
+
return url.startsWith(`${endpoint}/`) ? filesUrl + url.slice(endpoint.length) : url;
|
|
81
|
+
},
|
|
82
|
+
async remove(bucket, key) {
|
|
83
|
+
await writer.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
FileError,
|
|
89
|
+
accepts,
|
|
90
|
+
createFiles,
|
|
91
|
+
safeName
|
|
92
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type CloudError = {
|
|
2
|
+
code?: string;
|
|
3
|
+
status?: number;
|
|
4
|
+
} | null | undefined;
|
|
5
|
+
declare const AUTH_MESSAGES: Record<string, string>;
|
|
6
|
+
/** Plain words for a sign-in error, never the library's own text. */
|
|
7
|
+
declare function authMessage(error: CloudError, overrides?: Record<string, string>): string;
|
|
8
|
+
declare const MESSAGES: {
|
|
9
|
+
readonly unavailable: "This isn't available right now. Please try again later.";
|
|
10
|
+
readonly saveFailed: "Couldn't save right now. Please try again later.";
|
|
11
|
+
readonly uploadsPaused: "Uploads are paused right now. Please try again later.";
|
|
12
|
+
readonly fileType: "That type of file can't be uploaded here.";
|
|
13
|
+
readonly fileTooLarge: (mb: number) => string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { AUTH_MESSAGES, type CloudError, MESSAGES, authMessage };
|
package/dist/index.js
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createNeonAuth } from '@neondatabase/auth/next/server';
|
|
2
|
+
|
|
3
|
+
type AuthOptions = {
|
|
4
|
+
url?: string;
|
|
5
|
+
secret?: string;
|
|
6
|
+
sessionDataTtl?: number;
|
|
7
|
+
};
|
|
8
|
+
type Auth = ReturnType<typeof createNeonAuth>;
|
|
9
|
+
declare function createAuth(options?: AuthOptions): Auth;
|
|
10
|
+
|
|
11
|
+
export { type Auth, type AuthOptions, createAuth };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
env
|
|
3
|
+
} from "./chunk-ESAV545E.js";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
import "server-only";
|
|
7
|
+
import { createNeonAuth } from "@neondatabase/auth/next/server";
|
|
8
|
+
function createAuth(options = {}) {
|
|
9
|
+
return createNeonAuth({
|
|
10
|
+
baseUrl: options.url ?? env("WEBTWIZZ_AUTH_URL"),
|
|
11
|
+
cookies: {
|
|
12
|
+
secret: options.secret ?? env("WEBTWIZZ_AUTH_SECRET"),
|
|
13
|
+
sessionDataTtl: options.sessionDataTtl
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
createAuth
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webtwizz/cloud",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sign-ins, database and file storage for sites built on WebTwizz Cloud",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/WebTwizz/webtwizz-cloud.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://webtwizz.com",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"webtwizz",
|
|
14
|
+
"auth",
|
|
15
|
+
"postgres",
|
|
16
|
+
"storage",
|
|
17
|
+
"nextjs"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"AI_GUIDE.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./server": {
|
|
31
|
+
"types": "./dist/server.d.ts",
|
|
32
|
+
"default": "./dist/server.js"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./dist/client.d.ts",
|
|
36
|
+
"default": "./dist/client.js"
|
|
37
|
+
},
|
|
38
|
+
"./db": {
|
|
39
|
+
"types": "./dist/db.d.ts",
|
|
40
|
+
"default": "./dist/db.js"
|
|
41
|
+
},
|
|
42
|
+
"./files": {
|
|
43
|
+
"types": "./dist/files.d.ts",
|
|
44
|
+
"default": "./dist/files.js"
|
|
45
|
+
},
|
|
46
|
+
"./AI_GUIDE.md": "./AI_GUIDE.md",
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
54
|
+
"release": "npm run build && changeset publish",
|
|
55
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public",
|
|
62
|
+
"provenance": true
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"next": ">=16.0.0",
|
|
66
|
+
"react": ">=18.0.0"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@aws-sdk/client-s3": "3.1148.0",
|
|
70
|
+
"@aws-sdk/s3-request-presigner": "3.1148.0",
|
|
71
|
+
"@neondatabase/auth": "0.5.0-beta",
|
|
72
|
+
"postgres": "3.4.9",
|
|
73
|
+
"server-only": "0.0.1"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@changesets/cli": "3.0.3",
|
|
77
|
+
"@types/node": "26.6.4",
|
|
78
|
+
"@types/react": "19.3.0",
|
|
79
|
+
"next": "16.4.0",
|
|
80
|
+
"react": "19.3.0",
|
|
81
|
+
"react-dom": "19.3.0",
|
|
82
|
+
"tsup": "8.5.1",
|
|
83
|
+
"typescript": "5.9.3",
|
|
84
|
+
"vitest": "4.1.11"
|
|
85
|
+
}
|
|
86
|
+
}
|