@wervt/nuxt 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/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @wervt/nuxt
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
+ Nuxt module for apps hosted on [wervt](https://wervt.app): each app runs in its own isolate with
4
+ its own Postgres schema, Drizzle, and live data through Electric shapes.
5
+
6
+ ```bash
7
+ npm create wervt@latest my-app
8
+ ```
9
+
10
+ or add it to an existing Nuxt app:
11
+
12
+ ```ts [nuxt.config.ts]
13
+ export default defineNuxtConfig({
14
+ modules: ['@wervt/nuxt'],
15
+ })
16
+ ```
17
+
18
+ - `useDb()` (server): Drizzle on the app's own schema, with migrations from `drizzle/`.
19
+ - `proxyShape()` (server) and `useShape()` (app): live rows from Postgres via Electric.
20
+ - `useWervtUser()`: the signed-in wervt user, on the server and in the app.
21
+ - `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
22
+ - `nuxt dev` starts against the local wervt stack (Postgres, Electric) and a dev user.
23
+
24
+ Deploy with [`@wervt/cli`](https://www.npmjs.com/package/@wervt/cli): `wervt deploy`.
25
+
26
+ Docs: https://wervt.app
package/dist/dev.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import "nuxt/schema";
2
+ //#region src/dev.d.ts
3
+ export interface DevOptions {
4
+ /** Superuser URL of the local wervt Postgres. */
5
+ adminUrl?: string;
6
+ /** WERVT_DB_SECRET of the local setup. */
7
+ dbSecret?: string;
8
+ electricUrl?: string;
9
+ electricSecret?: string;
10
+ /**
11
+ * Who `/_wervt/login` signs in as during `nuxt dev` (there's no wervt auth locally).
12
+ * Defaults to a dev user with a Twitch account linked.
13
+ */
14
+ user?: {
15
+ id: string;
16
+ email: string;
17
+ name?: string;
18
+ image?: string;
19
+ accounts?: Record<string, string>;
20
+ };
21
+ }
22
+ //#endregion
package/dist/dev.js ADDED
@@ -0,0 +1,73 @@
1
+ import { appDatabaseUrl, dbRole, provisionDatabase } from "./provision.js";
2
+ import { useLogger } from "nuxt/kit";
3
+ import { existsSync, watch } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ //#region src/dev.ts
7
+ const DEFAULTS = {
8
+ adminUrl: "postgres://postgres:postgres@localhost:5432/wervt",
9
+ dbSecret: "dev-secret",
10
+ electricUrl: "http://localhost:3030",
11
+ electricSecret: "dev-electric-secret"
12
+ };
13
+ const logger = useLogger("wervt");
14
+ /**
15
+ * `nuxt dev` against the local compose stack: provisions the app's role and schema,
16
+ * runs its migrations (again whenever `drizzle/` changes) and points the dev server
17
+ * at the app's database and Electric.
18
+ */
19
+ async function setupDev(nuxt, app, options) {
20
+ const config = {
21
+ adminUrl: process.env.WERVT_PG_ADMIN_URL ?? options.adminUrl ?? DEFAULTS.adminUrl,
22
+ dbSecret: process.env.WERVT_DB_SECRET ?? options.dbSecret ?? DEFAULTS.dbSecret,
23
+ electricUrl: process.env.ELECTRIC_URL ?? options.electricUrl ?? DEFAULTS.electricUrl,
24
+ electricSecret: process.env.ELECTRIC_SECRET ?? options.electricSecret ?? DEFAULTS.electricSecret
25
+ };
26
+ process.env.WERVT_DEV_USER ??= JSON.stringify(options.user ?? {
27
+ id: "dev",
28
+ email: "dev@wervt.localhost",
29
+ name: "Dev",
30
+ accounts: { twitch: "dev" }
31
+ });
32
+ const migrationsFolder = join(nuxt.options.rootDir, "drizzle");
33
+ if (!existsSync(migrationsFolder)) return;
34
+ const provision = {
35
+ app,
36
+ adminUrl: config.adminUrl,
37
+ secret: config.dbSecret,
38
+ migrationsFolder
39
+ };
40
+ const migrate = async () => {
41
+ try {
42
+ await provisionDatabase(provision);
43
+ logger.success(`Database \`${dbRole(app)}\` is up to date`);
44
+ } catch (e) {
45
+ logger.warn(`Could not migrate \`${dbRole(app)}\`. Is the wervt stack running (docker compose up -d)?`, e instanceof Error ? e.message : e);
46
+ }
47
+ };
48
+ await migrate();
49
+ process.env.DATABASE_URL ??= await appDatabaseUrl(provision);
50
+ process.env.WERVT_DB_SCHEMA ??= dbRole(app);
51
+ process.env.ELECTRIC_URL ??= config.electricUrl;
52
+ process.env.ELECTRIC_SECRET ??= config.electricSecret;
53
+ let timer;
54
+ const watcher = watch(migrationsFolder, { recursive: true }, (_event, file) => {
55
+ if (!file?.endsWith(".sql")) return;
56
+ clearTimeout(timer);
57
+ timer = setTimeout(() => {
58
+ logger.info("Migrations changed, migrating…");
59
+ migrate();
60
+ }, 300);
61
+ });
62
+ nuxt.hook("close", () => watcher.close());
63
+ }
64
+ /** App name from the module option or package.json, normalised to `[a-z0-9-]+`. */
65
+ async function resolveAppName(rootDir, name) {
66
+ if (!name) {
67
+ const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8").catch(() => "{}"));
68
+ name = String(pkg.name ?? "app").replace(/^@[^/]+\//, "");
69
+ }
70
+ return name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
71
+ }
72
+ //#endregion
73
+ export { resolveAppName, setupDev };
@@ -0,0 +1,11 @@
1
+ import { DevOptions } from "./dev.js";
2
+ //#region src/module.d.ts
3
+ export interface ModuleOptions {
4
+ /** App name on wervt. Defaults to the package.json name. */
5
+ app?: string;
6
+ /** Local stack used by `nuxt dev`. Defaults match the wervt compose setup. */
7
+ dev?: DevOptions;
8
+ }
9
+ declare const _default: import("nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
10
+ //#endregion
11
+ export { _default as default };
package/dist/module.js ADDED
@@ -0,0 +1,142 @@
1
+ import { resolveAppName, setupDev } from "./dev.js";
2
+ import { addImports, addPlugin, addServerHandler, createResolver, defineNuxtModule, hasNuxtModule, installModule } from "nuxt/kit";
3
+ import { builtinModules } from "node:module";
4
+ import evlog from "evlog/nuxt";
5
+ //#region src/module.ts
6
+ const CONTENT_DATABASE = {
7
+ type: "postgres",
8
+ url: "{{DATABASE_URL}}"
9
+ };
10
+ var module_default = defineNuxtModule({
11
+ meta: {
12
+ name: "@wervt/nuxt",
13
+ configKey: "wervt"
14
+ },
15
+ async setup(options, nuxt) {
16
+ const resolver = createResolver(import.meta.url);
17
+ const nitro = nuxt.options.nitro;
18
+ if (!nuxt.options.dev) nitro.preset ||= "deno-server";
19
+ if (!hasNuxtModule("evlog/nuxt", nuxt)) {
20
+ const app = await resolveAppName(nuxt.options.rootDir, options.app);
21
+ const config = nuxt.options;
22
+ config.evlog = {
23
+ env: { service: app },
24
+ redact: true,
25
+ exclude: [
26
+ "/_wervt/**",
27
+ "/_nuxt/**",
28
+ "/__nuxt_error",
29
+ "/_fonts/**"
30
+ ],
31
+ ...config.evlog
32
+ };
33
+ await installModule(evlog, void 0, nuxt);
34
+ }
35
+ const useContentDatabase = !nuxt.options.dev && hasNuxtModule("@nuxt/content", nuxt);
36
+ if (useContentDatabase) {
37
+ const options = nuxt.options;
38
+ options.content = {
39
+ ...options.content,
40
+ database: CONTENT_DATABASE
41
+ };
42
+ }
43
+ nuxt.hook("nitro:config", (config) => {
44
+ const options = config;
45
+ options.externals = {
46
+ ...options.externals,
47
+ inline: [
48
+ ...options.externals?.inline ?? [],
49
+ resolver.resolve("./runtime"),
50
+ /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@electric-sql\/client|evlog)(\/|$)/
51
+ ]
52
+ };
53
+ const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
54
+ options.esbuild = {
55
+ ...options.esbuild,
56
+ options: {
57
+ ...options.esbuild?.options,
58
+ exclude: [exclude].flat().map((pattern) => typeof pattern === "string" ? pattern : exceptRuntime(pattern))
59
+ }
60
+ };
61
+ if (useContentDatabase) {
62
+ options.experimental = {
63
+ ...options.experimental,
64
+ envExpansion: true
65
+ };
66
+ if (options.runtimeConfig?.content) options.runtimeConfig.content.database = CONTENT_DATABASE;
67
+ }
68
+ if (!nuxt.options.dev) options.rollupConfig = {
69
+ ...options.rollupConfig,
70
+ plugins: [...[options.rollupConfig?.plugins ?? []].flat(), prefixNodeBuiltins()]
71
+ };
72
+ options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject: {
73
+ Buffer: ["node:buffer", "Buffer"],
74
+ setImmediate: ["node:timers", "setImmediate"],
75
+ clearImmediate: ["node:timers", "clearImmediate"]
76
+ } }];
77
+ });
78
+ if (nuxt.options.dev && !nuxt.options._prepare) {
79
+ const app = await resolveAppName(nuxt.options.rootDir, options.app);
80
+ await setupDev(nuxt, app, options.dev ?? {});
81
+ }
82
+ if (nuxt.options.dev) for (const route of ["/_wervt/login", "/_wervt/logout"]) addServerHandler({
83
+ route,
84
+ handler: resolver.resolve("./runtime/server/dev-auth")
85
+ });
86
+ addServerHandler({
87
+ route: "/_wervt/user",
88
+ method: "get",
89
+ handler: resolver.resolve("./runtime/server/user-route")
90
+ });
91
+ addPlugin({
92
+ src: resolver.resolve("./runtime/app/plugins/user.client"),
93
+ mode: "client"
94
+ });
95
+ nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
96
+ addImports([
97
+ {
98
+ name: "useShape",
99
+ from: resolver.resolve("./runtime/app/useShape")
100
+ },
101
+ {
102
+ name: "useWervtUser",
103
+ from: resolver.resolve("./runtime/app/useWervtUser")
104
+ },
105
+ {
106
+ name: "wervtLoginUrl",
107
+ from: resolver.resolve("./runtime/app/useWervtUser")
108
+ },
109
+ {
110
+ name: "wervtLogoutUrl",
111
+ from: resolver.resolve("./runtime/app/useWervtUser")
112
+ }
113
+ ]);
114
+ }
115
+ });
116
+ const BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
117
+ /** Rewrites `from 'os'` / `import('os')` to `node:os` in the output chunks. */
118
+ function prefixNodeBuiltins() {
119
+ const IMPORT_RE = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])([a-z_/]+)\2/g;
120
+ return {
121
+ name: "wervt:prefix-node-builtins",
122
+ renderChunk(code) {
123
+ let changed = false;
124
+ const out = code.replace(IMPORT_RE, (match, lead, quote, id) => {
125
+ if (!BUILTINS.has(id)) return match;
126
+ changed = true;
127
+ return `${lead}${quote}node:${id}${quote}`;
128
+ });
129
+ return changed ? {
130
+ code: out,
131
+ map: null
132
+ } : null;
133
+ }
134
+ };
135
+ }
136
+ /** `pattern`, except for files of this module's runtime. */
137
+ function exceptRuntime(pattern) {
138
+ if (pattern.source.startsWith("^")) return pattern;
139
+ return new RegExp(`^(?![\\s\\S]*[\\\\/]@wervt[\\\\/]nuxt[\\\\/]src[\\\\/]runtime[\\\\/])[\\s\\S]*?(?:${pattern.source})`, pattern.flags);
140
+ }
141
+ //#endregion
142
+ export { module_default as default };
@@ -0,0 +1,26 @@
1
+ //#region src/provision.d.ts
2
+ export interface ProvisionOptions {
3
+ /** App name, `[a-z0-9-]+`. */
4
+ app: string;
5
+ /** Superuser connection URL to the `wervt` database. */
6
+ adminUrl: string;
7
+ /** Secret the role password is derived from (WERVT_DB_SECRET). */
8
+ secret: string;
9
+ /** Drizzle migrations folder. Skipped when undefined. */
10
+ migrationsFolder?: string;
11
+ }
12
+ export declare function dbRole(app: string): string;
13
+ export declare function hmac(secret: string, message: string): Promise<string>;
14
+ export declare function dbPassword(app: string, secret: string): Promise<string>;
15
+ /** Connection URL for the app's own role, on the same host and database as `adminUrl`. */
16
+ export declare function appDatabaseUrl({ app, adminUrl, secret }: ProvisionOptions): Promise<string>;
17
+ /**
18
+ * Creates (or updates) the app's role and schema and runs its Drizzle migrations as
19
+ * that role. Each app gets a role and schema named `app_<name>`; the role's
20
+ * search_path is its own schema, it can't create schemas or touch `public`, and it can
21
+ * only connect to the wervt database.
22
+ */
23
+ export declare function provisionDatabase(options: ProvisionOptions): Promise<void>;
24
+ /** Drops the app's schema (all its data) and role. */
25
+ export declare function dropDatabase({ app, adminUrl }: Pick<ProvisionOptions, 'app' | 'adminUrl'>): Promise<void>;
26
+ //#endregion
@@ -0,0 +1,123 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
3
+ import postgres from "postgres";
4
+ //#region src/provision.ts
5
+ function dbRole(app) {
6
+ return `app_${app.replaceAll("-", "_")}`;
7
+ }
8
+ async function hmac(secret, message) {
9
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
10
+ name: "HMAC",
11
+ hash: "SHA-256"
12
+ }, false, ["sign"]);
13
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
14
+ return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ function dbPassword(app, secret) {
17
+ return hmac(secret, app);
18
+ }
19
+ /** Connection URL for the app's own role, on the same host and database as `adminUrl`. */
20
+ async function appDatabaseUrl({ app, adminUrl, secret }) {
21
+ const url = new URL(adminUrl);
22
+ url.username = dbRole(app);
23
+ url.password = await dbPassword(app, secret);
24
+ return url.href;
25
+ }
26
+ /**
27
+ * Creates (or updates) the app's role and schema and runs its Drizzle migrations as
28
+ * that role. Each app gets a role and schema named `app_<name>`; the role's
29
+ * search_path is its own schema, it can't create schemas or touch `public`, and it can
30
+ * only connect to the wervt database.
31
+ */
32
+ async function provisionDatabase(options) {
33
+ const { app, adminUrl, secret, migrationsFolder } = options;
34
+ const role = dbRole(app);
35
+ const password = await dbPassword(app, secret);
36
+ const database = new URL(adminUrl).pathname.slice(1);
37
+ const admin = postgres(adminUrl, {
38
+ max: 1,
39
+ onnotice: () => {}
40
+ });
41
+ try {
42
+ const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
43
+ const exists = Boolean(row?.exists);
44
+ await admin.unsafe(`${exists ? "alter" : "create"} role "${role}" login password '${password}' connection limit 10`);
45
+ await admin.unsafe(`create schema if not exists "${role}" authorization "${role}"`);
46
+ await claimSchema(admin, role);
47
+ await admin.unsafe(`alter role "${role}" set search_path = "${role}"`);
48
+ for (const db of [
49
+ database,
50
+ "postgres",
51
+ "template1"
52
+ ]) await admin.unsafe(`revoke all on database "${db}" from public`);
53
+ await admin.unsafe(`grant connect on database "${database}" to "${role}"`);
54
+ await admin.unsafe(`revoke all on schema public from public`);
55
+ if (!migrationsFolder) return;
56
+ await admin.unsafe(`grant create on database "${database}" to "${role}"`);
57
+ const client = postgres(await appDatabaseUrl(options), {
58
+ max: 1,
59
+ onnotice: () => {}
60
+ });
61
+ try {
62
+ await migrate(drizzle({ client }), {
63
+ migrationsFolder,
64
+ migrationsSchema: role
65
+ });
66
+ } finally {
67
+ await client.end();
68
+ await admin.unsafe(`revoke create on database "${database}" from "${role}"`);
69
+ }
70
+ } finally {
71
+ await admin.end();
72
+ }
73
+ }
74
+ /**
75
+ * Gives the role its schema and everything in it back. A dump restored with `--no-owner`
76
+ * (Coolify's backups) leaves all of it owned by the restoring superuser.
77
+ */
78
+ async function claimSchema(admin, role) {
79
+ await admin.unsafe(`alter schema "${role}" owner to "${role}"`);
80
+ const objects = await admin`
81
+ select case c.relkind when 'S' then 'sequence' when 'v' then 'view'
82
+ when 'm' then 'materialized view' when 'f' then 'foreign table' else 'table' end as kind,
83
+ format('%I.%I', n.nspname, c.relname) as name
84
+ from pg_class c join pg_namespace n on n.oid = c.relnamespace
85
+ where n.nspname = ${role} and c.relkind in ('r', 'p', 'v', 'm', 'f', 'S')
86
+ and c.relowner <> ${role}::regrole
87
+ -- identity and serial sequences follow their table
88
+ and not exists (select 1 from pg_depend d where d.classid = 'pg_class'::regclass
89
+ and d.objid = c.oid and d.deptype in ('a', 'i'))
90
+ union all
91
+ select case t.typtype when 'd' then 'domain' else 'type' end, format('%I.%I', n.nspname, t.typname)
92
+ from pg_type t join pg_namespace n on n.oid = t.typnamespace
93
+ where n.nspname = ${role} and t.typtype in ('e', 'd')
94
+ and t.typowner <> ${role}::regrole
95
+ union all
96
+ select case p.prokind when 'p' then 'procedure' when 'a' then 'aggregate' else 'function' end,
97
+ p.oid::regprocedure::text
98
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
99
+ where n.nspname = ${role} and p.proowner <> ${role}::regrole`;
100
+ for (const { kind, name } of objects) await admin.unsafe(`alter ${kind} ${name} owner to "${role}"`);
101
+ }
102
+ /** Drops the app's schema (all its data) and role. */
103
+ async function dropDatabase({ app, adminUrl }) {
104
+ const role = dbRole(app);
105
+ const database = new URL(adminUrl).pathname.slice(1);
106
+ const admin = postgres(adminUrl, {
107
+ max: 1,
108
+ onnotice: () => {}
109
+ });
110
+ try {
111
+ await admin`select pg_terminate_backend(pid) from pg_stat_activity where usename = ${role}`;
112
+ await admin.unsafe(`drop schema if exists "${role}" cascade`);
113
+ const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
114
+ if (Boolean(row?.exists)) {
115
+ await admin.unsafe(`revoke all on database "${database}" from "${role}"`);
116
+ await admin.unsafe(`drop role "${role}"`);
117
+ }
118
+ } finally {
119
+ await admin.end();
120
+ }
121
+ }
122
+ //#endregion
123
+ export { appDatabaseUrl, dbPassword, dbRole, dropDatabase, hmac, provisionDatabase };
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/app/plugins/user.client.d.ts
2
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,18 @@
1
+ import { useWervtUser } from "../useWervtUser.js";
2
+ import { defineNuxtPlugin } from "#app";
3
+ //#region src/runtime/app/plugins/user.client.ts
4
+ var user_client_default = defineNuxtPlugin({
5
+ name: "wervt:user",
6
+ async setup(nuxtApp) {
7
+ if (nuxtApp.payload.serverRendered) return;
8
+ const user = useWervtUser();
9
+ try {
10
+ const res = await fetch("/_wervt/user");
11
+ user.value = res.ok ? (await res.json()).user : null;
12
+ } catch {
13
+ user.value = null;
14
+ }
15
+ }
16
+ });
17
+ //#endregion
18
+ export { user_client_default as default };
@@ -0,0 +1,26 @@
1
+ import { Row } from "@electric-sql/client";
2
+ //#region src/runtime/app/useShape.d.ts
3
+ export interface UseShapeOptions {
4
+ /** Payload key. Defaults to the path. */
5
+ key?: string;
6
+ /** Keep the shape live on the client. Defaults to true. */
7
+ live?: boolean;
8
+ }
9
+ /**
10
+ * Live rows of an Electric shape served by one of the app's shape routes
11
+ * (see `proxyShape`). The snapshot is loaded during SSR and sent in the payload;
12
+ * the client resumes the stream from the snapshot's offset, so rows are not
13
+ * fetched twice.
14
+ *
15
+ * @example const { data: todos } = await useShape<Todo>('/api/shapes/todos')
16
+ */
17
+ export declare function useShape<T extends Row<unknown>>(path: string, options?: UseShapeOptions): Promise<{
18
+ data: import("vue").ShallowRef<T[], T[]>;
19
+ error: import("vue").Ref<unknown, unknown>;
20
+ isLive: import("vue").Ref<boolean, boolean>;
21
+ }> & {
22
+ data: import("vue").ShallowRef<T[], T[]>;
23
+ error: import("vue").Ref<unknown, unknown>;
24
+ isLive: import("vue").Ref<boolean, boolean>;
25
+ };
26
+ //#endregion
@@ -0,0 +1,140 @@
1
+ import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
2
+ import { getCurrentScope, onScopeDispose, ref, shallowRef } from "vue";
3
+ import { onNuxtReady, useAsyncData, useRequestEvent, useRequestFetch } from "#app";
4
+ //#region src/runtime/app/useShape.ts
5
+ /**
6
+ * Live rows of an Electric shape served by one of the app's shape routes
7
+ * (see `proxyShape`). The snapshot is loaded during SSR and sent in the payload;
8
+ * the client resumes the stream from the snapshot's offset, so rows are not
9
+ * fetched twice.
10
+ *
11
+ * @example const { data: todos } = await useShape<Todo>('/api/shapes/todos')
12
+ */
13
+ function useShape(path, options = {}) {
14
+ const fetchClient = import.meta.server ? inProcessFetch() : void 0;
15
+ const asyncData = useAsyncData(options.key ?? `wervt-shape:${path}`, () => loadSnapshot(path, fetchClient));
16
+ const rows = /* @__PURE__ */ new Map();
17
+ const data = shallowRef([]);
18
+ const error = ref(null);
19
+ const isLive = ref(false);
20
+ const result = {
21
+ data,
22
+ error,
23
+ isLive
24
+ };
25
+ const ready = Promise.resolve(asyncData).then(() => {
26
+ for (const [key, row] of asyncData.data.value?.rows ?? []) rows.set(key, row);
27
+ data.value = [...rows.values()];
28
+ error.value = asyncData.error.value ?? null;
29
+ if (error.value) console.error(`[wervt] useShape('${path}') failed:`, error.value);
30
+ return result;
31
+ });
32
+ if (import.meta.client && options.live !== false) {
33
+ let stream;
34
+ const controller = new AbortController();
35
+ onNuxtReady(async () => {
36
+ await ready;
37
+ if (controller.signal.aborted) return;
38
+ const snapshot = asyncData.data.value;
39
+ stream = new ShapeStream({
40
+ url: absolute(path),
41
+ offset: snapshot?.offset,
42
+ handle: snapshot?.handle,
43
+ signal: controller.signal,
44
+ onError: (e) => {
45
+ error.value = e;
46
+ }
47
+ });
48
+ stream.subscribe((messages) => {
49
+ if (applyMessages(rows, messages)) data.value = [...rows.values()];
50
+ isLive.value = stream?.isUpToDate ?? false;
51
+ });
52
+ });
53
+ if (getCurrentScope()) onScopeDispose(() => {
54
+ controller.abort();
55
+ stream?.unsubscribeAll();
56
+ });
57
+ }
58
+ return Object.assign(ready, result);
59
+ }
60
+ async function loadSnapshot(path, fetchClient) {
61
+ const rows = /* @__PURE__ */ new Map();
62
+ let fail = () => {};
63
+ const stream = new ShapeStream({
64
+ url: absolute(path),
65
+ subscribe: false,
66
+ fetchClient,
67
+ onError: (e) => fail(e)
68
+ });
69
+ await new Promise((resolve, reject) => {
70
+ fail = reject;
71
+ stream.subscribe((messages) => {
72
+ applyMessages(rows, messages);
73
+ if (stream.isUpToDate) resolve();
74
+ }, (e) => reject(e));
75
+ });
76
+ stream.unsubscribeAll();
77
+ return {
78
+ rows: [...rows],
79
+ offset: stream.lastOffset,
80
+ handle: stream.shapeHandle
81
+ };
82
+ }
83
+ /** Applies Electric change messages to `rows`. Returns whether anything changed. */
84
+ function applyMessages(rows, messages) {
85
+ let changed = false;
86
+ for (const message of messages) if (isChangeMessage(message)) {
87
+ const { key, value } = message;
88
+ switch (message.headers.operation) {
89
+ case "insert":
90
+ rows.set(key, value);
91
+ break;
92
+ case "update":
93
+ rows.set(key, {
94
+ ...rows.get(key),
95
+ ...value
96
+ });
97
+ break;
98
+ case "delete": rows.delete(key);
99
+ }
100
+ changed = true;
101
+ } else if (isControlMessage(message) && message.headers.control === "must-refetch") {
102
+ rows.clear();
103
+ changed = true;
104
+ }
105
+ return changed;
106
+ }
107
+ /**
108
+ * During SSR, calls the shape route in-process with the request's headers (cookies, auth).
109
+ * Nitro 2 (Nuxt 4.6) has `event.fetch`, which returns a Response; on Nitro 3 (Nuxt 5) the
110
+ * request's `$fetch` has `.raw`.
111
+ */
112
+ function inProcessFetch() {
113
+ const event = useRequestEvent();
114
+ if (typeof event?.fetch === "function") {
115
+ const eventFetch = event.fetch;
116
+ return ((input, init) => {
117
+ const url = new URL(input instanceof Request ? input.url : input);
118
+ return eventFetch(url.pathname + url.search, init);
119
+ });
120
+ }
121
+ const requestFetch = useRequestFetch();
122
+ return (async (input, init) => {
123
+ const url = new URL(input instanceof Request ? input.url : input);
124
+ const res = await requestFetch.raw(url.pathname + url.search, {
125
+ method: init?.method ?? "GET",
126
+ headers: init?.headers,
127
+ responseType: "stream",
128
+ ignoreResponseError: true
129
+ });
130
+ return new Response(res._data, {
131
+ status: res.status,
132
+ headers: res.headers
133
+ });
134
+ });
135
+ }
136
+ function absolute(path) {
137
+ return new URL(path, import.meta.client ? location.origin : "http://localhost").href;
138
+ }
139
+ //#endregion
140
+ export { useShape };
@@ -0,0 +1,13 @@
1
+ import { WervtUser } from "../shared/user.js";
2
+ //#region src/runtime/app/useWervtUser.d.ts
3
+ /**
4
+ * The signed-in wervt user, or `null`. Read during SSR and kept in the payload; apps without
5
+ * SSR load it from `/_wervt/user` before they render. Public apps get one when the visitor
6
+ * is signed in.
7
+ */
8
+ export declare function useWervtUser(): import("vue").Ref<WervtUser | null, WervtUser | null>;
9
+ /** Sign in with wervt, then come back to `redirect` (a path in this app). */
10
+ export declare const wervtLoginUrl: (redirect?: string) => string;
11
+ /** Sign out of wervt (all apps), then come back to `redirect`. */
12
+ export declare const wervtLogoutUrl: (redirect?: string) => string;
13
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { DEV_LOGIN_COOKIE, devUser, parseWervtUser } from "../shared/user.js";
2
+ import { useCookie, useRequestHeaders, useState } from "#app";
3
+ //#region src/runtime/app/useWervtUser.ts
4
+ /**
5
+ * The signed-in wervt user, or `null`. Read during SSR and kept in the payload; apps without
6
+ * SSR load it from `/_wervt/user` before they render. Public apps get one when the visitor
7
+ * is signed in.
8
+ */
9
+ function useWervtUser() {
10
+ const devLogin = import.meta.dev ? useCookie(DEV_LOGIN_COOKIE) : void 0;
11
+ return useState("wervt-user", () => import.meta.server ? parseWervtUser(useRequestHeaders(["x-wervt-user"])["x-wervt-user"]) ?? devUser(String(devLogin?.value) === "1") ?? null : null);
12
+ }
13
+ /** Sign in with wervt, then come back to `redirect` (a path in this app). */
14
+ const wervtLoginUrl = (redirect = "/") => `/_wervt/login?redirect=${encodeURIComponent(redirect)}`;
15
+ /** Sign out of wervt (all apps), then come back to `redirect`. */
16
+ const wervtLogoutUrl = (redirect = "/") => `/_wervt/logout?redirect=${encodeURIComponent(redirect)}`;
17
+ //#endregion
18
+ export { useWervtUser, wervtLoginUrl, wervtLogoutUrl };
@@ -0,0 +1,19 @@
1
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { AnyRelations, EmptyRelations } from "drizzle-orm";
3
+ //#region src/runtime/server/db.d.ts
4
+ /**
5
+ * Drizzle client for the app's database. In production the wervt runtime injects
6
+ * DATABASE_URL for the app's own role, whose search_path is its own schema, so
7
+ * tables are declared without a schema.
8
+ *
9
+ * Pass the app's relations (from `defineRelations`) to use `db.query.*`. All clients
10
+ * share one connection pool.
11
+ *
12
+ * @example
13
+ * const db = useDb({ relations })
14
+ * const room = await db.query.rooms.findFirst({ where: { id }, with: { players: true } })
15
+ */
16
+ export declare function useDb<TRelations extends AnyRelations = EmptyRelations>(options?: {
17
+ relations?: TRelations;
18
+ }): PostgresJsDatabase<TRelations>;
19
+ //#endregion
@@ -0,0 +1,36 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import postgres from "postgres";
3
+ //#region src/runtime/server/db.ts
4
+ let client;
5
+ const clients = /* @__PURE__ */ new Map();
6
+ /**
7
+ * Drizzle client for the app's database. In production the wervt runtime injects
8
+ * DATABASE_URL for the app's own role, whose search_path is its own schema, so
9
+ * tables are declared without a schema.
10
+ *
11
+ * Pass the app's relations (from `defineRelations`) to use `db.query.*`. All clients
12
+ * share one connection pool.
13
+ *
14
+ * @example
15
+ * const db = useDb({ relations })
16
+ * const room = await db.query.rooms.findFirst({ where: { id }, with: { players: true } })
17
+ */
18
+ function useDb(options) {
19
+ if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is not set");
20
+ client ??= postgres(process.env.DATABASE_URL, {
21
+ max: 2,
22
+ idle_timeout: 20
23
+ });
24
+ const relations = options?.relations;
25
+ let db = clients.get(relations);
26
+ if (!db) {
27
+ db = drizzle({
28
+ client,
29
+ relations
30
+ });
31
+ clients.set(relations, db);
32
+ }
33
+ return db;
34
+ }
35
+ //#endregion
36
+ export { useDb };
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/server/dev-auth.d.ts
2
+ declare const _default: import("nuxt/server").EventHandler<string>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,15 @@
1
+ import { DEV_LOGIN_COOKIE } from "../shared/user.js";
2
+ import { defineEventHandler, deleteCookie, getQuery, sendRedirect, setCookie } from "nuxt/server";
3
+ //#region src/runtime/server/dev-auth.ts
4
+ var dev_auth_default = defineEventHandler((event) => {
5
+ const { redirect } = getQuery(event);
6
+ const target = typeof redirect === "string" && redirect.startsWith("/") ? redirect : "/";
7
+ if (new URL(event.req.url).pathname === "/_wervt/login") setCookie(event, DEV_LOGIN_COOKIE, "1", {
8
+ path: "/",
9
+ sameSite: "lax"
10
+ });
11
+ else deleteCookie(event, DEV_LOGIN_COOKIE, { path: "/" });
12
+ return sendRedirect(event, target);
13
+ });
14
+ //#endregion
15
+ export { dev_auth_default as default };
@@ -0,0 +1,6 @@
1
+ import { WervtUser } from "../shared/user.js";
2
+ import { useDb } from "./db.js";
3
+ import { ShapeOptions, proxyShape } from "./shape.js";
4
+ import { useWervtUser } from "./user.js";
5
+ import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
6
+ export { type SafeResult, type ShapeOptions, type WervtUser, proxyShape, useDb, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -0,0 +1,5 @@
1
+ import { useDb } from "./db.js";
2
+ import { proxyShape } from "./shape.js";
3
+ import { useWervtUser } from "./user.js";
4
+ import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
5
+ export { proxyShape, useDb, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -0,0 +1,19 @@
1
+ import { RequestEvent } from "nuxt/server";
2
+ //#region src/runtime/server/shape.d.ts
3
+ export interface ShapeOptions {
4
+ /** Table name without schema. */
5
+ table: string;
6
+ /** SQL where clause. Use `$1`, `$2`, … placeholders with `params` for values. */
7
+ where?: string;
8
+ /** Values for the `$n` placeholders in `where`. */
9
+ params?: (string | number | boolean)[];
10
+ columns?: string[];
11
+ }
12
+ /**
13
+ * Serves an Electric shape from a route. Do auth checks before calling it.
14
+ *
15
+ * In production the request goes through the wervt runtime gateway, which pins the
16
+ * table to this app's schema. In dev it goes to Electric directly (ELECTRIC_URL).
17
+ */
18
+ export declare function proxyShape(event: RequestEvent, { table, where, params, columns }: ShapeOptions): Promise<Response>;
19
+ //#endregion
@@ -0,0 +1,55 @@
1
+ //#region src/runtime/server/shape.ts
2
+ const CLIENT_PARAMS = [
3
+ "offset",
4
+ "handle",
5
+ "live",
6
+ "live_sse",
7
+ "cursor",
8
+ "replica",
9
+ "log"
10
+ ];
11
+ /**
12
+ * Serves an Electric shape from a route. Do auth checks before calling it.
13
+ *
14
+ * In production the request goes through the wervt runtime gateway, which pins the
15
+ * table to this app's schema. In dev it goes to Electric directly (ELECTRIC_URL).
16
+ */
17
+ async function proxyShape(event, { table, where, params, columns }) {
18
+ const runtime = process.env.WERVT_RUNTIME_URL;
19
+ const url = runtime ? new URL("/_wervt/electric/v1/shape", runtime) : new URL("/v1/shape", process.env.ELECTRIC_URL ?? "http://localhost:3000");
20
+ const query = new URL(event.req.url).searchParams;
21
+ for (const key of CLIENT_PARAMS) {
22
+ const value = query.get(key);
23
+ if (value !== null) url.searchParams.set(key, value);
24
+ }
25
+ url.searchParams.set("table", runtime ? table : devTable(table));
26
+ if (where) url.searchParams.set("where", where);
27
+ params?.forEach((value, i) => url.searchParams.set(`params[${i + 1}]`, String(value)));
28
+ if (columns) url.searchParams.set("columns", columns.map(quoteIdentifier).join(","));
29
+ if (!runtime && process.env.ELECTRIC_SECRET) url.searchParams.set("secret", process.env.ELECTRIC_SECRET);
30
+ const headers = {};
31
+ if (runtime) {
32
+ headers["x-wervt-app"] = process.env.WERVT_APP ?? "";
33
+ headers["x-wervt-token"] = process.env.WERVT_TOKEN ?? "";
34
+ }
35
+ const res = await fetch(url, {
36
+ headers,
37
+ signal: event.req.signal
38
+ });
39
+ const out = new Headers(res.headers);
40
+ out.delete("content-encoding");
41
+ out.delete("content-length");
42
+ return new Response(res.body, {
43
+ status: res.status,
44
+ headers: out
45
+ });
46
+ }
47
+ function devTable(table) {
48
+ const schema = process.env.WERVT_DB_SCHEMA;
49
+ return schema ? `${schema}.${table}` : table;
50
+ }
51
+ function quoteIdentifier(name) {
52
+ return /^[a-z_][a-z0-9_]*$/.test(name) ? name : `"${name.replaceAll("\"", "\"\"")}"`;
53
+ }
54
+ //#endregion
55
+ export { proxyShape };
@@ -0,0 +1,8 @@
1
+ import { WervtUser } from "../shared/user.js";
2
+ import "./user.js";
3
+ //#region src/runtime/server/user-route.d.ts
4
+ declare const _default: import("nuxt/server").EventHandler<{
5
+ user: WervtUser | null;
6
+ }>;
7
+ //#endregion
8
+ export { _default as default };
@@ -0,0 +1,9 @@
1
+ import { useWervtUser } from "./user.js";
2
+ import { defineEventHandler } from "nuxt/server";
3
+ //#region src/runtime/server/user-route.ts
4
+ var user_route_default = defineEventHandler((event) => {
5
+ event.res.headers.set("cache-control", "private, no-store");
6
+ return { user: useWervtUser(event) ?? null };
7
+ });
8
+ //#endregion
9
+ export { user_route_default as default };
@@ -0,0 +1,12 @@
1
+ import { DEV_LOGIN_COOKIE, WervtUser, parseWervtUser } from "../shared/user.js";
2
+ import "./user.js";
3
+ import { RequestEvent } from "nuxt/server";
4
+ //#region src/runtime/server/user.d.ts
5
+ /**
6
+ * The signed-in wervt user, or `undefined` when nobody is signed in.
7
+ * Private apps always have one (the runtime checked the login and the allow-list); public
8
+ * apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
9
+ */
10
+ export declare const useWervtUser: (event: RequestEvent) => WervtUser | undefined;
11
+ //#endregion
12
+ export { DEV_LOGIN_COOKIE, type WervtUser, parseWervtUser };
@@ -0,0 +1,11 @@
1
+ import { DEV_LOGIN_COOKIE, devUser, parseWervtUser } from "../shared/user.js";
2
+ import { getCookie } from "nuxt/server";
3
+ //#region src/runtime/server/user.ts
4
+ /**
5
+ * The signed-in wervt user, or `undefined` when nobody is signed in.
6
+ * Private apps always have one (the runtime checked the login and the allow-list); public
7
+ * apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
8
+ */
9
+ const useWervtUser = (event) => parseWervtUser(event.req.headers.get("x-wervt-user")) ?? devUser(getCookie(event, "wervt_dev_login") === "1");
10
+ //#endregion
11
+ export { DEV_LOGIN_COOKIE, parseWervtUser, useWervtUser };
@@ -0,0 +1,40 @@
1
+ import { RequestEvent } from "nuxt/server";
2
+ import * as v from "valibot";
3
+ //#region src/runtime/server/validate.d.ts
4
+ type Schema = v.GenericSchema | v.GenericSchemaAsync;
5
+ type Shape = v.ObjectEntriesAsync;
6
+ type Input = Schema | Shape;
7
+ type Output<T extends Input> = T extends Schema ? v.InferOutput<T> : T extends Shape ? v.InferOutput<v.ObjectSchemaAsync<T, undefined>> : never;
8
+ export type SafeResult<T extends Input> = {
9
+ success: true;
10
+ data: Output<T>;
11
+ issues?: undefined;
12
+ } | {
13
+ success: false;
14
+ data?: undefined;
15
+ issues: v.FlatErrors<undefined>;
16
+ };
17
+ /** Parsed JSON body. Throws a 400 with flattened issues when invalid. */
18
+ export declare const useValidatedBody: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
19
+ /** Query parameters (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
20
+ export declare const useValidatedQuery: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
21
+ /** Route params like `[id]` (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
22
+ export declare const useValidatedParams: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
23
+ /** Like `useValidatedBody`, but returns `{ success, data | issues }` instead of throwing. */
24
+ export declare const useSafeValidatedBody: <T extends Input>(event: RequestEvent, schema: T) => Promise<SafeResult<T>>;
25
+ export declare const useSafeValidatedQuery: <T extends Input>(event: RequestEvent, schema: T) => Promise<SafeResult<T>>;
26
+ export declare const useSafeValidatedParams: <T extends Input>(event: RequestEvent, schema: T) => Promise<SafeResult<T>>;
27
+ /** Schemas for values that arrive as strings (query, route params, form fields). */
28
+ export declare const vh: {
29
+ /** `'true'` / `'false'` → boolean. */
30
+ boolAsString: v.SchemaWithPipe<readonly [v.PicklistSchema<["true", "false"], undefined>, v.TransformAction<"true" | "false", boolean>]>;
31
+ /** HTML checkbox: `'on'` or missing → boolean. */
32
+ checkboxAsString: v.SchemaWithPipe<readonly [v.OptionalSchema<v.LiteralSchema<"on", undefined>, undefined>, v.TransformAction<"on" | undefined, boolean>]>;
33
+ /** Integer string → number. */
34
+ intAsString: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>, v.TransformAction<any, number>, v.SafeIntegerAction<number, undefined>]>;
35
+ /** Numeric string → number. */
36
+ numAsString: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DecimalAction<string, undefined>, v.TransformAction<any, number>]>;
37
+ /** Positive integer id (e.g. a serial primary key) → number. */
38
+ id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DigitsAction<string, undefined>, v.TransformAction<any, number>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
39
+ };
40
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { createError, getQuery, getRouterParams, readBody } from "nuxt/server";
2
+ import * as v from "valibot";
3
+ //#region src/runtime/server/validate.ts
4
+ function toSchema(input) {
5
+ return "kind" in input && input.kind === "schema" ? input : v.objectAsync(input);
6
+ }
7
+ async function safeValidate(input, data) {
8
+ const result = await v.safeParseAsync(toSchema(input), data);
9
+ return result.success ? {
10
+ success: true,
11
+ data: result.output
12
+ } : {
13
+ success: false,
14
+ issues: v.flatten(result.issues)
15
+ };
16
+ }
17
+ async function validate(input, data) {
18
+ const result = await safeValidate(input, data);
19
+ if (result.success) return result.data;
20
+ throw createError({
21
+ status: 400,
22
+ statusText: "Validation failed",
23
+ message: "Validation failed",
24
+ data: result.issues
25
+ });
26
+ }
27
+ /** Parsed JSON body. Throws a 400 with flattened issues when invalid. */
28
+ const useValidatedBody = (event, schema) => readBody(event).then((body) => validate(schema, body));
29
+ /** Query parameters (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
30
+ const useValidatedQuery = (event, schema) => validate(schema, getQuery(event));
31
+ /** Route params like `[id]` (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
32
+ const useValidatedParams = (event, schema) => validate(schema, getRouterParams(event, { decode: true }));
33
+ /** Like `useValidatedBody`, but returns `{ success, data | issues }` instead of throwing. */
34
+ const useSafeValidatedBody = (event, schema) => readBody(event).then((body) => safeValidate(schema, body));
35
+ const useSafeValidatedQuery = (event, schema) => safeValidate(schema, getQuery(event));
36
+ const useSafeValidatedParams = (event, schema) => safeValidate(schema, getRouterParams(event, { decode: true }));
37
+ /** Schemas for values that arrive as strings (query, route params, form fields). */
38
+ const vh = {
39
+ /** `'true'` / `'false'` → boolean. */
40
+ boolAsString: v.pipe(v.picklist(["true", "false"]), v.transform((s) => s === "true")),
41
+ /** HTML checkbox: `'on'` or missing → boolean. */
42
+ checkboxAsString: v.pipe(v.optional(v.literal("on")), v.transform((s) => s === "on")),
43
+ /** Integer string → number. */
44
+ intAsString: v.pipe(v.string(), v.regex(/^-?\d+$/), v.transform(Number), v.safeInteger()),
45
+ /** Numeric string → number. */
46
+ numAsString: v.pipe(v.string(), v.decimal(), v.transform(Number)),
47
+ /** Positive integer id (e.g. a serial primary key) → number. */
48
+ id: v.pipe(v.string(), v.digits(), v.transform(Number), v.safeInteger(), v.minValue(1))
49
+ };
50
+ //#endregion
51
+ export { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
@@ -0,0 +1,16 @@
1
+ //#region src/runtime/shared/user.d.ts
2
+ export interface WervtUser {
3
+ id: string;
4
+ email: string;
5
+ name?: string;
6
+ image?: string;
7
+ /** Linked login accounts, provider id to the user's id there, e.g. `{ twitch: '1234' }`. */
8
+ accounts?: Record<string, string>;
9
+ }
10
+ /** Cookie set by the dev stand-in for `/_wervt/login` (see `server/dev-auth.ts`). */
11
+ export declare const DEV_LOGIN_COOKIE = "wervt_dev_login";
12
+ /** Parses the `x-wervt-user` header the runtime sets (and strips from client requests). */
13
+ export declare function parseWervtUser(header: string | null | undefined): WervtUser | undefined;
14
+ /** During `nuxt dev`: the configured dev user, after `/_wervt/login`. */
15
+ export declare function devUser(loggedIn: boolean): WervtUser | undefined;
16
+ //#endregion
@@ -0,0 +1,20 @@
1
+ //#region src/runtime/shared/user.ts
2
+ /** Cookie set by the dev stand-in for `/_wervt/login` (see `server/dev-auth.ts`). */
3
+ const DEV_LOGIN_COOKIE = "wervt_dev_login";
4
+ /** Parses the `x-wervt-user` header the runtime sets (and strips from client requests). */
5
+ function parseWervtUser(header) {
6
+ if (!header) return void 0;
7
+ try {
8
+ return JSON.parse(decodeURIComponent(header));
9
+ } catch {
10
+ return;
11
+ }
12
+ }
13
+ /** During `nuxt dev`: the configured dev user, after `/_wervt/login`. */
14
+ function devUser(loggedIn) {
15
+ const user = process.env.WERVT_DEV_USER;
16
+ if (!import.meta.dev || !loggedIn || !user) return void 0;
17
+ return JSON.parse(user);
18
+ }
19
+ //#endregion
20
+ export { DEV_LOGIN_COOKIE, devUser, parseWervtUser };
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@wervt/nuxt",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
5
+ "keywords": [
6
+ "drizzle",
7
+ "electric",
8
+ "nuxt",
9
+ "nuxt-module",
10
+ "postgres",
11
+ "wervt"
12
+ ],
13
+ "homepage": "https://wervt.app",
14
+ "license": "MIT",
15
+ "author": "Niki2k1",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/wervt-app/wervt.git",
19
+ "directory": "packages/nuxt"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/module.d.ts",
29
+ "default": "./dist/module.js"
30
+ },
31
+ "./server": {
32
+ "types": "./dist/runtime/server/index.d.ts",
33
+ "default": "./dist/runtime/server/index.js"
34
+ },
35
+ "./provision": {
36
+ "types": "./dist/provision.d.ts",
37
+ "default": "./dist/provision.js"
38
+ }
39
+ },
40
+ "dependencies": {
41
+ "@electric-sql/client": "^1.5.28",
42
+ "evlog": "^2.30.0",
43
+ "postgres": "^3.4.9",
44
+ "valibot": "^1.5.0"
45
+ },
46
+ "devDependencies": {
47
+ "drizzle-orm": "1.0.0-rc.4",
48
+ "nitro": "3.0.260903-beta",
49
+ "nuxt": "npm:nuxt-nightly@5x",
50
+ "vue": "^3.5.43"
51
+ },
52
+ "peerDependencies": {
53
+ "drizzle-orm": "1.0.0-rc.4",
54
+ "nuxt": ">=4.2"
55
+ },
56
+ "scripts": {
57
+ "build": "vp pack"
58
+ }
6
59
  }