@wervt/nuxt 0.3.9 → 0.3.11
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/dist/brand.d.ts +16 -3
- package/dist/brand.js +22 -6
- package/dist/module.d.ts +3 -14
- package/dist/module.js +24 -59
- package/dist/provision.d.ts +1 -77
- package/dist/provision.js +1 -238
- package/dist/runtime/app/plugins/analytics.client.js +2 -2
- package/dist/runtime/app/plugins/feedback.client.js +11 -22
- package/dist/runtime/app/plugins/user.client.js +2 -7
- package/dist/runtime/app/plugins/webmcp.client.js +4 -41
- package/dist/runtime/app/snapshot.d.ts +2 -15
- package/dist/runtime/app/snapshot.js +1 -63
- package/dist/runtime/app/useShape.js +1 -1
- package/dist/runtime/app/useWervtFeedback.d.ts +1 -13
- package/dist/runtime/app/useWervtFeedback.js +3 -11
- package/dist/runtime/app/useWervtTrack.js +4 -8
- package/dist/runtime/app/useWervtUser.d.ts +3 -2
- package/dist/runtime/app/useWervtUser.js +3 -2
- package/dist/runtime/email/components.d.ts +18 -0
- package/dist/runtime/email/components.js +15 -4
- package/dist/runtime/email/invite.d.ts +13 -0
- package/dist/runtime/email/invite.js +30 -14
- package/dist/runtime/i18n.d.ts +3 -0
- package/dist/runtime/i18n.js +3 -0
- package/dist/runtime/server/access.d.ts +3 -22
- package/dist/runtime/server/access.js +7 -24
- package/dist/runtime/server/anthropic.d.ts +2 -7
- package/dist/runtime/server/anthropic.js +6 -37
- package/dist/runtime/server/db.d.ts +2 -17
- package/dist/runtime/server/db.js +1 -35
- package/dist/runtime/server/errors.d.ts +6 -0
- package/dist/runtime/server/errors.js +21 -0
- package/dist/runtime/server/identity.d.ts +3 -9
- package/dist/runtime/server/identity.js +4 -23
- package/dist/runtime/server/index.d.ts +1 -1
- package/dist/runtime/server/index.js +1 -1
- package/dist/runtime/server/mail.js +11 -29
- package/dist/runtime/server/mcp-claims.d.ts +2 -3
- package/dist/runtime/server/mcp-claims.js +1 -17
- package/dist/runtime/server/seed.d.ts +2 -16
- package/dist/runtime/server/seed.js +1 -26
- package/dist/runtime/server/shape.d.ts +2 -2
- package/dist/runtime/server/shape.js +5 -45
- package/dist/runtime/server/task-handlers.d.ts +1 -5
- package/dist/runtime/server/task-handlers.js +3 -9
- package/dist/runtime/server/task-run-nitro2.d.ts +1 -5
- package/dist/runtime/server/task-run-nitro3.d.ts +1 -5
- package/dist/runtime/server/tasks.d.ts +2 -32
- package/dist/runtime/server/tasks.js +1 -9
- package/dist/runtime/server/user-route.d.ts +1 -3
- package/dist/runtime/server/user.js +8 -11
- package/dist/runtime/shared/agent-token.d.ts +2 -7
- package/dist/runtime/shared/agent-token.js +1 -16
- package/dist/runtime/shared/i18n.d.ts +28 -0
- package/dist/runtime/shared/i18n.js +54 -0
- package/dist/runtime/shared/messages.d.ts +15 -0
- package/dist/runtime/shared/messages.js +33 -0
- package/dist/runtime/shared/shape.d.ts +2 -17
- package/dist/runtime/shared/shape.js +1 -15
- package/dist/runtime/shared/user.d.ts +3 -19
- package/dist/runtime/shared/user.js +1 -9
- package/package.json +9 -1
- package/dist/dev.d.ts +0 -31
- package/dist/dev.js +0 -158
- package/dist/jsdoc-imports.js +0 -25
- package/dist/seed.js +0 -51
- package/dist/storage.d.ts +0 -72
- package/dist/storage.js +0 -362
package/dist/brand.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { Locale } from "./runtime/shared/i18n.js";
|
|
1
2
|
export interface BrandOptions {
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The product name the login shows ("Sign in to …"). Defaults to the app's `app.head.title` up
|
|
5
|
+
* to its first separator, so "Lightdrop · Drop it. Get it." gives "Lightdrop".
|
|
6
|
+
*/
|
|
3
7
|
name?: string;
|
|
4
8
|
/**
|
|
5
9
|
* A wide logo for the top of the login page: a file in `public/` or an https URL. Without one,
|
|
@@ -8,10 +12,14 @@ export interface BrandOptions {
|
|
|
8
12
|
logo?: string;
|
|
9
13
|
/**
|
|
10
14
|
* A square icon for the tab and the code mail: a file in `public/` or an https URL. PNG for the
|
|
11
|
-
* mail, since Gmail and Outlook don't show SVG. Defaults to the app's
|
|
15
|
+
* mail, since Gmail and Outlook don't show SVG. Defaults to the app's PNG icon in `app.head`
|
|
16
|
+
* (`<link rel="icon">` or `apple-touch-icon`), else its first `<link rel="icon">`.
|
|
12
17
|
*/
|
|
13
18
|
icon?: string;
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The login's primary color as hex (`#ff5a1f`). Without it the login keeps wervt's: the
|
|
21
|
+
* `theme-color` meta tag is usually the page background, not an accent.
|
|
22
|
+
*/
|
|
15
23
|
color?: string;
|
|
16
24
|
/**
|
|
17
25
|
* The small "Secured by wervt" under the login and in the mail footer. On by default: it says
|
|
@@ -23,6 +31,11 @@ export interface BrandOptions {
|
|
|
23
31
|
* them by default. Providers the server has no keys for stay hidden either way.
|
|
24
32
|
*/
|
|
25
33
|
signIn?: SignInMethod[];
|
|
34
|
+
/**
|
|
35
|
+
* The language of the login, the code mail and the consent page (`'de'`). Without it they
|
|
36
|
+
* follow the visitor's browser: German for a German preference, English otherwise.
|
|
37
|
+
*/
|
|
38
|
+
lang?: Locale;
|
|
26
39
|
}
|
|
27
40
|
export declare const SIGN_IN_METHODS: readonly ['google', 'github', 'discord', 'twitch', 'spotify', 'passkey', 'email'];
|
|
28
41
|
export type SignInMethod = (typeof SIGN_IN_METHODS)[number];
|
package/dist/brand.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { headIcon
|
|
1
|
+
import { headIcon } from "./preview.js";
|
|
2
|
+
import { isLocale } from "./runtime/shared/i18n.js";
|
|
2
3
|
// src/brand.ts
|
|
3
4
|
const SIGN_IN_METHODS = [
|
|
4
5
|
"google",
|
|
@@ -16,21 +17,36 @@ function signInMethods(value) {
|
|
|
16
17
|
return methods.length ? methods : void 0;
|
|
17
18
|
}
|
|
18
19
|
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
20
|
+
/** Image types Gmail and Outlook show; SVG and ICO stay blank there. */
|
|
21
|
+
const MAIL_IMAGE = /\.(png|jpe?g|gif)(\?.*)?$/i;
|
|
22
|
+
const TITLE_SEPARATOR = /\s+[·|–—-]\s+|\s*[·|–—]\s*|:\s+/;
|
|
23
|
+
/** The product name in a page title, without its tagline. */
|
|
24
|
+
function titleName(title) {
|
|
25
|
+
return title.split(TITLE_SEPARATOR)[0].trim() || title.trim();
|
|
26
|
+
}
|
|
27
|
+
function mailIcon(head) {
|
|
28
|
+
return (head.link?.find((link) => [
|
|
29
|
+
"icon",
|
|
30
|
+
"shortcut icon",
|
|
31
|
+
"apple-touch-icon"
|
|
32
|
+
].includes(String(link.rel)) && typeof link.href === "string" && MAIL_IMAGE.test(link.href)))?.href;
|
|
33
|
+
}
|
|
19
34
|
/** The login's branding for the app, or undefined while the app keeps wervt's. */
|
|
20
35
|
function brandManifest(option, head, app, favicon) {
|
|
21
36
|
if (!option) return void 0;
|
|
22
37
|
const custom = option === true ? {} : option;
|
|
23
38
|
const pick = (value, fallback) => value?.trim() || fallback;
|
|
24
|
-
const title = typeof head.title === "string" && head.title.trim() ? head.title
|
|
25
|
-
const color =
|
|
39
|
+
const title = typeof head.title === "string" && head.title.trim() ? titleName(head.title) : app;
|
|
40
|
+
const color = custom.color?.trim();
|
|
26
41
|
const signIn = signInMethods(custom.signIn);
|
|
27
42
|
return {
|
|
28
43
|
name: pick(custom.name, title).slice(0, 60),
|
|
29
44
|
logo: pick(custom.logo, void 0),
|
|
30
|
-
icon: pick(custom.icon, headIcon(head) ?? favicon),
|
|
45
|
+
icon: pick(custom.icon, mailIcon(head) ?? headIcon(head) ?? favicon),
|
|
31
46
|
color: color && HEX_COLOR.test(color) ? color : void 0,
|
|
32
47
|
...custom.securedByWervt === false && { securedByWervt: false },
|
|
33
|
-
...signIn && { signIn }
|
|
48
|
+
...signIn && { signIn },
|
|
49
|
+
...isLocale(custom.lang) && { lang: custom.lang }
|
|
34
50
|
};
|
|
35
51
|
}
|
|
36
|
-
export { HEX_COLOR, SIGN_IN_METHODS, brandManifest, signInMethods };
|
|
52
|
+
export { HEX_COLOR, MAIL_IMAGE, SIGN_IN_METHODS, brandManifest, signInMethods, titleName };
|
package/dist/module.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { DevOptions } from "./dev.js";
|
|
2
1
|
import { PreviewOptions } from "./preview.js";
|
|
3
2
|
import { BrandOptions } from "./brand.js";
|
|
3
|
+
import { TasksManifest } from "@wervt/core";
|
|
4
|
+
import { DevOptions } from "@wervt/platform/dev";
|
|
4
5
|
export interface ModuleOptions {
|
|
5
6
|
/** App name on wervt. Defaults to the package.json name. */
|
|
6
7
|
app?: string;
|
|
@@ -76,17 +77,5 @@ export interface McpOptions {
|
|
|
76
77
|
/** How to use the tools together, e.g. which one to call first. Clients pass it to the model. */
|
|
77
78
|
instructions?: string;
|
|
78
79
|
}
|
|
79
|
-
/** `server/wervt-tasks.json` in the build: the app's tasks and schedules, for the control plane. */
|
|
80
|
-
export interface TasksManifest {
|
|
81
|
-
tasks: {
|
|
82
|
-
name: string;
|
|
83
|
-
description?: string;
|
|
84
|
-
}[];
|
|
85
|
-
schedules: {
|
|
86
|
-
cron: string;
|
|
87
|
-
tasks: string[];
|
|
88
|
-
}[];
|
|
89
|
-
timezone?: string;
|
|
90
|
-
}
|
|
91
80
|
declare const _default: import("nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
92
|
-
export { _default as default };
|
|
81
|
+
export { type TasksManifest, _default as default };
|
package/dist/module.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { SEED_TASK, unscheduleSeed } from "./seed.js";
|
|
2
|
-
import { resolveAppName, setupDev } from "./dev.js";
|
|
3
1
|
import { setupStudioPreview } from "./studio-preview.js";
|
|
4
2
|
import { PROCESS_ENV_TYPES } from "./env-types.js";
|
|
5
3
|
import { dependencyNames, ogImageDefaults, ogImageWarning } from "./og-image.js";
|
|
6
4
|
import { iconDefaults, iconSources, iconWarning } from "./icons.js";
|
|
7
|
-
import { stripJsdocImports } from "./jsdoc-imports.js";
|
|
8
5
|
import { findRouteTools } from "./route-tools.js";
|
|
9
6
|
import { localFiles, previewManifest } from "./preview.js";
|
|
10
|
-
import {
|
|
7
|
+
import { LOCALES } from "./runtime/shared/i18n.js";
|
|
8
|
+
import { MAIL_IMAGE, SIGN_IN_METHODS, brandManifest } from "./brand.js";
|
|
11
9
|
import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
|
|
10
|
+
import { BUNDLE_MANIFEST } from "@wervt/core";
|
|
11
|
+
import { SEED_TASK, resolveAppName, setupDev, unscheduleSeed } from "@wervt/platform/dev";
|
|
12
12
|
import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, updateTemplates, useLogger } from "nuxt/kit";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
15
|
-
import {
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
16
|
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
17
17
|
import evlog from "evlog/nuxt";
|
|
18
|
+
import { DENO_INJECT, denoPlugins } from "@wervt/nitro/build";
|
|
18
19
|
// src/module.ts
|
|
19
20
|
const CONTENT_DATABASE = {
|
|
20
21
|
type: "postgres",
|
|
@@ -86,7 +87,7 @@ var module_default = defineNuxtModule({
|
|
|
86
87
|
inline: [
|
|
87
88
|
...options.externals?.inline ?? [],
|
|
88
89
|
resolver.resolve("./runtime"),
|
|
89
|
-
/(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
|
|
90
|
+
/(^|\/node_modules\/)(@wervt\/(core|client)|drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
|
|
90
91
|
]
|
|
91
92
|
};
|
|
92
93
|
const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
|
|
@@ -106,18 +107,9 @@ var module_default = defineNuxtModule({
|
|
|
106
107
|
}
|
|
107
108
|
if (!nuxt.options.dev) options.rollupConfig = {
|
|
108
109
|
...options.rollupConfig,
|
|
109
|
-
plugins: [
|
|
110
|
-
...[options.rollupConfig?.plugins ?? []].flat(),
|
|
111
|
-
prefixNodeBuiltins(),
|
|
112
|
-
configurableWaitUntil(),
|
|
113
|
-
stripJsdocImports()
|
|
114
|
-
]
|
|
110
|
+
plugins: [...[options.rollupConfig?.plugins ?? []].flat(), ...denoPlugins()]
|
|
115
111
|
};
|
|
116
|
-
options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject:
|
|
117
|
-
Buffer: ["node:buffer", "Buffer"],
|
|
118
|
-
setImmediate: ["node:timers", "setImmediate"],
|
|
119
|
-
clearImmediate: ["node:timers", "clearImmediate"]
|
|
120
|
-
} }];
|
|
112
|
+
options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject: DENO_INJECT }];
|
|
121
113
|
});
|
|
122
114
|
if (nitroMajor >= 3 && !nuxt.options.dev) nuxt.hook("vite:extendConfig", (config, { isServer }) => {
|
|
123
115
|
const ssr = config.environments?.ssr;
|
|
@@ -202,6 +194,10 @@ var module_default = defineNuxtModule({
|
|
|
202
194
|
};
|
|
203
195
|
await mkdir(nitroOptions.output.serverDir, { recursive: true });
|
|
204
196
|
await writeFile(join(nitroOptions.output.serverDir, "wervt-tasks.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
197
|
+
await writeFile(join(dirname(nitroOptions.output.serverDir), BUNDLE_MANIFEST), `${JSON.stringify({
|
|
198
|
+
version: 2,
|
|
199
|
+
framework: "nuxt"
|
|
200
|
+
}, null, 2)}\n`);
|
|
205
201
|
});
|
|
206
202
|
});
|
|
207
203
|
}
|
|
@@ -282,7 +278,15 @@ var module_default = defineNuxtModule({
|
|
|
282
278
|
if (nuxt.options.dev && process.env.WERVT_PREVIEW) setupStudioPreview(nuxt);
|
|
283
279
|
if (nuxt.options.dev && !nuxt.options._prepare) {
|
|
284
280
|
const app = await resolveAppName(nuxt.options.rootDir, options.app);
|
|
285
|
-
await setupDev(
|
|
281
|
+
await setupDev({
|
|
282
|
+
rootDir: nuxt.options.rootDir,
|
|
283
|
+
onClose: (fn) => nuxt.hook("close", fn),
|
|
284
|
+
restart: () => void nuxt.callHook("restart"),
|
|
285
|
+
onListen: (fn) => nuxt.hook("listen", (_server, listener) => {
|
|
286
|
+
if (listener.url) fn(listener.url);
|
|
287
|
+
}),
|
|
288
|
+
logger
|
|
289
|
+
}, app, options.dev ?? {}, { storage: useAblage });
|
|
286
290
|
}
|
|
287
291
|
addTypeTemplate({
|
|
288
292
|
filename: "types/wervt-email.d.ts",
|
|
@@ -397,7 +401,9 @@ var module_default = defineNuxtModule({
|
|
|
397
401
|
icon: brand.icon,
|
|
398
402
|
image: brand.logo
|
|
399
403
|
} : false)) if (!existsSync(join(publicDir, file))) logger.warn(`The login branding names \`${file}\`, which isn't in public/.`);
|
|
404
|
+
if (brand?.icon && !MAIL_IMAGE.test(brand.icon)) logger.warn(`The login branding's icon \`${brand.icon}\` won't show in the sign-in mail: Gmail and Outlook need PNG or JPEG. Set \`wervt.brand.icon\` to one.`);
|
|
400
405
|
if (typeof options.brand === "object" && options.brand.color && !brand?.color) logger.warn("`wervt.brand.color` must be a hex color like `#ff5a1f`; it is ignored.");
|
|
406
|
+
if (typeof options.brand === "object" && options.brand.lang && !brand?.lang) logger.warn(`\`wervt.brand.lang\` must be one of ${LOCALES.join(", ")}; the login follows the browser instead.`);
|
|
401
407
|
if (typeof options.brand === "object" && options.brand.signIn) {
|
|
402
408
|
const unknown = options.brand.signIn.filter((m) => !SIGN_IN_METHODS.includes(m));
|
|
403
409
|
if (unknown.length) logger.warn(`\`wervt.brand.signIn\` ignores ${unknown.map((m) => `\`${m}\``).join(", ")}; it knows ${SIGN_IN_METHODS.join(", ")}.`);
|
|
@@ -506,47 +512,6 @@ function hasPackage(dir, name) {
|
|
|
506
512
|
}
|
|
507
513
|
/** evlog's Nitro 2 plugin and error handler, as evlog/nuxt resolves them. */
|
|
508
514
|
const EVLOG_NITRO2_RE = /([\\/]evlog[\\/]dist[\\/]nitro)[\\/](plugin|errorHandler)(\.mjs)?$/;
|
|
509
|
-
const BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
|
|
510
|
-
/** Rewrites `from 'os'` / `import('os')` to `node:os` in the output chunks. */
|
|
511
|
-
function prefixNodeBuiltins() {
|
|
512
|
-
const IMPORT_RE = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])([a-z_/]+)\2/g;
|
|
513
|
-
return {
|
|
514
|
-
name: "wervt:prefix-node-builtins",
|
|
515
|
-
renderChunk(code) {
|
|
516
|
-
let changed = false;
|
|
517
|
-
const out = code.replace(IMPORT_RE, (match, lead, quote, id) => {
|
|
518
|
-
if (!BUILTINS.has(id)) return match;
|
|
519
|
-
changed = true;
|
|
520
|
-
return `${lead}${quote}node:${id}${quote}`;
|
|
521
|
-
});
|
|
522
|
-
return changed ? {
|
|
523
|
-
code: out,
|
|
524
|
-
map: null
|
|
525
|
-
} : null;
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* srvx (Nitro 3's server layer) pins `waitUntil` on each request as a non-configurable property.
|
|
531
|
-
* Edge Runtime hands the app a Proxy whose `get` returns its own `waitUntil`, which breaks the
|
|
532
|
-
* proxy invariant for non-configurable properties: reading `req.waitUntil` throws. Nitro's
|
|
533
|
-
* error handler reads it, so every thrown error (404, 400, …) became a 500. Configurable, the
|
|
534
|
-
* proxy may answer with its own function, which works the same.
|
|
535
|
-
*/
|
|
536
|
-
function configurableWaitUntil() {
|
|
537
|
-
const PIN_RE = /waitUntil:\s*\{\s*value:\s*(this\.#wait\?\.waitUntil)\s*\}/g;
|
|
538
|
-
return {
|
|
539
|
-
name: "wervt:configurable-wait-until",
|
|
540
|
-
renderChunk(code) {
|
|
541
|
-
if (!code.includes("waitUntil")) return null;
|
|
542
|
-
const out = code.replace(PIN_RE, "waitUntil: { value: $1, configurable: true, writable: true }");
|
|
543
|
-
return out === code ? null : {
|
|
544
|
-
code: out,
|
|
545
|
-
map: null
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
515
|
/** `pattern`, except for files of this module's runtime. */
|
|
551
516
|
function exceptRuntime(pattern) {
|
|
552
517
|
if (pattern.source.startsWith("^")) return pattern;
|
package/dist/provision.d.ts
CHANGED
|
@@ -1,77 +1 @@
|
|
|
1
|
-
|
|
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
|
-
/** Timeouts for the app's role; DEFAULT_ROLE_TIMEOUTS where not given. */
|
|
12
|
-
timeouts?: Partial<RoleTimeouts>;
|
|
13
|
-
}
|
|
14
|
-
/** Postgres durations (`30s`, `500ms`, `2min`) for the app's role; `0` turns one off. */
|
|
15
|
-
export interface RoleTimeouts {
|
|
16
|
-
/** Longest a single statement may run. */
|
|
17
|
-
statement: string;
|
|
18
|
-
/** Longest a transaction may sit idle, holding its locks and its pooled connection. */
|
|
19
|
-
idleInTransaction: string;
|
|
20
|
-
}
|
|
21
|
-
export declare const DEFAULT_ROLE_TIMEOUTS: RoleTimeouts;
|
|
22
|
-
/** A Postgres duration as the env gives it, e.g. `WERVT_STATEMENT_TIMEOUT`; the fallback when empty. */
|
|
23
|
-
export declare function parseDuration(value: string | undefined, fallback: string, name?: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Sets the timeouts on an existing app role, for apps that aren't redeployed. New sessions get
|
|
26
|
-
* them; open ones keep theirs. Returns false when the app has no role.
|
|
27
|
-
*/
|
|
28
|
-
export declare function setRoleTimeouts({ app, adminUrl, timeouts }: Pick<ProvisionOptions, 'app' | 'adminUrl' | 'timeouts'>): Promise<boolean>;
|
|
29
|
-
export declare function dbRole(app: string): string;
|
|
30
|
-
export declare function hmac(secret: string, message: string): Promise<string>;
|
|
31
|
-
/** The app's Garage bucket and S3 key, derived from WERVT_DB_SECRET like role passwords. */
|
|
32
|
-
export declare function storageCredentials(app: string, secret: string): Promise<{
|
|
33
|
-
bucket: string;
|
|
34
|
-
accessKeyId: string;
|
|
35
|
-
secretAccessKey: string;
|
|
36
|
-
}>;
|
|
37
|
-
export declare function dbPassword(app: string, secret: string): Promise<string>;
|
|
38
|
-
/** Connection URL for the app's own role, on the same host and database as `adminUrl`. */
|
|
39
|
-
export declare function appDatabaseUrl({ app, adminUrl, secret }: ProvisionOptions): Promise<string>;
|
|
40
|
-
/**
|
|
41
|
-
* Creates (or updates) the app's role and schema and runs its Drizzle migrations as
|
|
42
|
-
* that role. Each app gets a role and schema named `app_<name>`; the role's
|
|
43
|
-
* search_path is its own schema, it can't create schemas or touch `public`, and it can
|
|
44
|
-
* only connect to the wervt database.
|
|
45
|
-
*
|
|
46
|
-
* `fresh` is true when no migration had been applied before, so `nuxt dev` knows when to seed.
|
|
47
|
-
*/
|
|
48
|
-
export declare function provisionDatabase(options: ProvisionOptions): Promise<{
|
|
49
|
-
fresh: boolean;
|
|
50
|
-
}>;
|
|
51
|
-
/**
|
|
52
|
-
* Runs the Drizzle migrations as the app's own role, into its schema. The role needs CREATE on
|
|
53
|
-
* the database while this runs (see `provisionDatabase`). `fresh` is true when no migration had
|
|
54
|
-
* been applied before.
|
|
55
|
-
*/
|
|
56
|
-
export declare function migrateAs({ url, schema, migrationsFolder }: {
|
|
57
|
-
url: string;
|
|
58
|
-
schema: string;
|
|
59
|
-
migrationsFolder: string;
|
|
60
|
-
}): Promise<{
|
|
61
|
-
fresh: boolean;
|
|
62
|
-
}>;
|
|
63
|
-
/** The role PgBouncer signs in with to look up app roles' password hashes (`auth_query`). */
|
|
64
|
-
export declare const POOLER_ROLE = "pgbouncer";
|
|
65
|
-
/**
|
|
66
|
-
* Prepares Postgres for PgBouncer in front of the app isolates: a login role for PgBouncer and
|
|
67
|
-
* `pgbouncer.get_auth()`, which hands out the SCRAM hashes of app roles (`app_*`) only, never
|
|
68
|
-
* those of the superuser or other roles. App passwords are derived, not stored anywhere
|
|
69
|
-
* PgBouncer could read them, so it has to ask Postgres.
|
|
70
|
-
*/
|
|
71
|
-
export declare function setupPooler({ adminUrl, password }: {
|
|
72
|
-
adminUrl: string;
|
|
73
|
-
password: string;
|
|
74
|
-
}): Promise<void>;
|
|
75
|
-
/** Drops the app's schema (all its data) and role. */
|
|
76
|
-
export declare function dropDatabase({ app, adminUrl }: Pick<ProvisionOptions, 'app' | 'adminUrl'>): Promise<void>;
|
|
77
|
-
export { MAX_KEY_BYTES, type ObjectDetails, type ObjectListing, StorageError, type StorageOptions, type StorageQuota, type StorageUsage, type StoredObject, createStorage };
|
|
1
|
+
export * from "@wervt/platform";
|
package/dist/provision.js
CHANGED
|
@@ -1,238 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
|
-
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
4
|
-
import postgres from "postgres";
|
|
5
|
-
// src/provision.ts
|
|
6
|
-
const DEFAULT_ROLE_TIMEOUTS = {
|
|
7
|
-
statement: "30s",
|
|
8
|
-
idleInTransaction: "60s"
|
|
9
|
-
};
|
|
10
|
-
const DURATION_RE = /^\d+(?:ms|s|min|h)?$/;
|
|
11
|
-
/** A Postgres duration as the env gives it, e.g. `WERVT_STATEMENT_TIMEOUT`; the fallback when empty. */
|
|
12
|
-
function parseDuration(value, fallback, name = "timeout") {
|
|
13
|
-
const trimmed = value?.trim();
|
|
14
|
-
if (!trimmed) return fallback;
|
|
15
|
-
if (!DURATION_RE.test(trimmed)) throw new Error(`${name} must be a duration like 30s, 500ms or 2min (0 for none)`);
|
|
16
|
-
return trimmed;
|
|
17
|
-
}
|
|
18
|
-
function roleTimeouts(timeouts) {
|
|
19
|
-
const merged = {
|
|
20
|
-
...DEFAULT_ROLE_TIMEOUTS,
|
|
21
|
-
...timeouts
|
|
22
|
-
};
|
|
23
|
-
return {
|
|
24
|
-
statement: parseDuration(merged.statement, DEFAULT_ROLE_TIMEOUTS.statement),
|
|
25
|
-
idleInTransaction: parseDuration(merged.idleInTransaction, DEFAULT_ROLE_TIMEOUTS.idleInTransaction)
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
async function applyRoleTimeouts(admin, role, timeouts) {
|
|
29
|
-
await admin.unsafe(`alter role "${role}" set statement_timeout = '${timeouts.statement}'`);
|
|
30
|
-
await admin.unsafe(`alter role "${role}" set idle_in_transaction_session_timeout = '${timeouts.idleInTransaction}'`);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Sets the timeouts on an existing app role, for apps that aren't redeployed. New sessions get
|
|
34
|
-
* them; open ones keep theirs. Returns false when the app has no role.
|
|
35
|
-
*/
|
|
36
|
-
async function setRoleTimeouts({ app, adminUrl, timeouts }) {
|
|
37
|
-
const role = dbRole(app);
|
|
38
|
-
const admin = postgres(adminUrl, {
|
|
39
|
-
max: 1,
|
|
40
|
-
onnotice: () => {}
|
|
41
|
-
});
|
|
42
|
-
try {
|
|
43
|
-
const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
|
|
44
|
-
if (!row?.exists) return false;
|
|
45
|
-
await applyRoleTimeouts(admin, role, roleTimeouts(timeouts));
|
|
46
|
-
return true;
|
|
47
|
-
} finally {
|
|
48
|
-
await admin.end();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function dbRole(app) {
|
|
52
|
-
return `app_${app.replaceAll("-", "_")}`;
|
|
53
|
-
}
|
|
54
|
-
async function hmac(secret, message) {
|
|
55
|
-
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
56
|
-
name: "HMAC",
|
|
57
|
-
hash: "SHA-256"
|
|
58
|
-
}, false, ["sign"]);
|
|
59
|
-
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
|
|
60
|
-
return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
61
|
-
}
|
|
62
|
-
/** The app's Garage bucket and S3 key, derived from WERVT_DB_SECRET like role passwords. */
|
|
63
|
-
async function storageCredentials(app, secret) {
|
|
64
|
-
return {
|
|
65
|
-
bucket: `app-${app}`,
|
|
66
|
-
accessKeyId: `GK${(await hmac(secret, `s3-id:${app}`)).slice(0, 24)}`,
|
|
67
|
-
secretAccessKey: await hmac(secret, `s3-secret:${app}`)
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function dbPassword(app, secret) {
|
|
71
|
-
return hmac(secret, app);
|
|
72
|
-
}
|
|
73
|
-
/** Connection URL for the app's own role, on the same host and database as `adminUrl`. */
|
|
74
|
-
async function appDatabaseUrl({ app, adminUrl, secret }) {
|
|
75
|
-
const url = new URL(adminUrl);
|
|
76
|
-
url.username = dbRole(app);
|
|
77
|
-
url.password = await dbPassword(app, secret);
|
|
78
|
-
return url.href;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Creates (or updates) the app's role and schema and runs its Drizzle migrations as
|
|
82
|
-
* that role. Each app gets a role and schema named `app_<name>`; the role's
|
|
83
|
-
* search_path is its own schema, it can't create schemas or touch `public`, and it can
|
|
84
|
-
* only connect to the wervt database.
|
|
85
|
-
*
|
|
86
|
-
* `fresh` is true when no migration had been applied before, so `nuxt dev` knows when to seed.
|
|
87
|
-
*/
|
|
88
|
-
async function provisionDatabase(options) {
|
|
89
|
-
const { app, adminUrl, secret, migrationsFolder } = options;
|
|
90
|
-
const role = dbRole(app);
|
|
91
|
-
const password = await dbPassword(app, secret);
|
|
92
|
-
const database = new URL(adminUrl).pathname.slice(1);
|
|
93
|
-
const admin = postgres(adminUrl, {
|
|
94
|
-
max: 1,
|
|
95
|
-
onnotice: () => {}
|
|
96
|
-
});
|
|
97
|
-
try {
|
|
98
|
-
const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
|
|
99
|
-
const exists = Boolean(row?.exists);
|
|
100
|
-
await admin.unsafe(`${exists ? "alter" : "create"} role "${role}" login password '${password}' connection limit 10`);
|
|
101
|
-
await admin.unsafe(`create schema if not exists "${role}" authorization "${role}"`);
|
|
102
|
-
await claimSchema(admin, role);
|
|
103
|
-
await admin.unsafe(`alter role "${role}" set search_path = "${role}"`);
|
|
104
|
-
await applyRoleTimeouts(admin, role, roleTimeouts(options.timeouts));
|
|
105
|
-
for (const db of [
|
|
106
|
-
database,
|
|
107
|
-
"postgres",
|
|
108
|
-
"template1"
|
|
109
|
-
]) await admin.unsafe(`revoke all on database "${db}" from public`);
|
|
110
|
-
await admin.unsafe(`grant connect on database "${database}" to "${role}"`);
|
|
111
|
-
await admin.unsafe(`revoke all on schema public from public`);
|
|
112
|
-
if (!migrationsFolder) return { fresh: false };
|
|
113
|
-
await admin.unsafe(`grant create on database "${database}" to "${role}"`);
|
|
114
|
-
try {
|
|
115
|
-
return await migrateAs({
|
|
116
|
-
url: await appDatabaseUrl(options),
|
|
117
|
-
schema: role,
|
|
118
|
-
migrationsFolder
|
|
119
|
-
});
|
|
120
|
-
} finally {
|
|
121
|
-
await admin.unsafe(`revoke create on database "${database}" from "${role}"`);
|
|
122
|
-
}
|
|
123
|
-
} finally {
|
|
124
|
-
await admin.end();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Runs the Drizzle migrations as the app's own role, into its schema. The role needs CREATE on
|
|
129
|
-
* the database while this runs (see `provisionDatabase`). `fresh` is true when no migration had
|
|
130
|
-
* been applied before.
|
|
131
|
-
*/
|
|
132
|
-
async function migrateAs({ url, schema, migrationsFolder }) {
|
|
133
|
-
const client = postgres(url, {
|
|
134
|
-
max: 1,
|
|
135
|
-
onnotice: () => {}
|
|
136
|
-
});
|
|
137
|
-
try {
|
|
138
|
-
const table = `"${schema}"."__drizzle_migrations"`;
|
|
139
|
-
const [state] = await client`
|
|
140
|
-
select to_regclass(${table}) is not null as exists`;
|
|
141
|
-
let fresh = !state?.exists;
|
|
142
|
-
if (!fresh) {
|
|
143
|
-
const [row] = await client.unsafe(`select exists(select 1 from ${table}) as applied`);
|
|
144
|
-
fresh = !row?.applied;
|
|
145
|
-
}
|
|
146
|
-
await client.unsafe("set statement_timeout = 0; set idle_in_transaction_session_timeout = 0");
|
|
147
|
-
await migrate(drizzle({ client }), {
|
|
148
|
-
migrationsFolder,
|
|
149
|
-
migrationsSchema: schema
|
|
150
|
-
});
|
|
151
|
-
return { fresh };
|
|
152
|
-
} finally {
|
|
153
|
-
await client.end();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Gives the role its schema and everything in it back. A dump restored with `--no-owner`
|
|
158
|
-
* (Coolify's backups) leaves all of it owned by the restoring superuser.
|
|
159
|
-
*/
|
|
160
|
-
async function claimSchema(admin, role) {
|
|
161
|
-
await admin.unsafe(`alter schema "${role}" owner to "${role}"`);
|
|
162
|
-
const objects = await admin`
|
|
163
|
-
select case c.relkind when 'S' then 'sequence' when 'v' then 'view'
|
|
164
|
-
when 'm' then 'materialized view' when 'f' then 'foreign table' else 'table' end as kind,
|
|
165
|
-
format('%I.%I', n.nspname, c.relname) as name
|
|
166
|
-
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
167
|
-
where n.nspname = ${role} and c.relkind in ('r', 'p', 'v', 'm', 'f', 'S')
|
|
168
|
-
and c.relowner <> ${role}::regrole
|
|
169
|
-
-- identity and serial sequences follow their table
|
|
170
|
-
and not exists (select 1 from pg_depend d where d.classid = 'pg_class'::regclass
|
|
171
|
-
and d.objid = c.oid and d.deptype in ('a', 'i'))
|
|
172
|
-
union all
|
|
173
|
-
select case t.typtype when 'd' then 'domain' else 'type' end, format('%I.%I', n.nspname, t.typname)
|
|
174
|
-
from pg_type t join pg_namespace n on n.oid = t.typnamespace
|
|
175
|
-
where n.nspname = ${role} and t.typtype in ('e', 'd')
|
|
176
|
-
and t.typowner <> ${role}::regrole
|
|
177
|
-
union all
|
|
178
|
-
select case p.prokind when 'p' then 'procedure' when 'a' then 'aggregate' else 'function' end,
|
|
179
|
-
p.oid::regprocedure::text
|
|
180
|
-
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
181
|
-
where n.nspname = ${role} and p.proowner <> ${role}::regrole`;
|
|
182
|
-
for (const { kind, name } of objects) await admin.unsafe(`alter ${kind} ${name} owner to "${role}"`);
|
|
183
|
-
}
|
|
184
|
-
/** The role PgBouncer signs in with to look up app roles' password hashes (`auth_query`). */
|
|
185
|
-
const POOLER_ROLE = "pgbouncer";
|
|
186
|
-
/**
|
|
187
|
-
* Prepares Postgres for PgBouncer in front of the app isolates: a login role for PgBouncer and
|
|
188
|
-
* `pgbouncer.get_auth()`, which hands out the SCRAM hashes of app roles (`app_*`) only, never
|
|
189
|
-
* those of the superuser or other roles. App passwords are derived, not stored anywhere
|
|
190
|
-
* PgBouncer could read them, so it has to ask Postgres.
|
|
191
|
-
*/
|
|
192
|
-
async function setupPooler({ adminUrl, password }) {
|
|
193
|
-
const database = new URL(adminUrl).pathname.slice(1);
|
|
194
|
-
const admin = postgres(adminUrl, {
|
|
195
|
-
max: 1,
|
|
196
|
-
onnotice: () => {}
|
|
197
|
-
});
|
|
198
|
-
try {
|
|
199
|
-
const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${POOLER_ROLE})`;
|
|
200
|
-
await admin.unsafe(`${row?.exists ? "alter" : "create"} role "${POOLER_ROLE}" login password '${password.replaceAll("'", "''")}'`);
|
|
201
|
-
await admin.unsafe(`grant connect on database "${database}" to "${POOLER_ROLE}"`);
|
|
202
|
-
await admin.unsafe(`create schema if not exists "${POOLER_ROLE}"`);
|
|
203
|
-
await admin.unsafe(`revoke all on schema "${POOLER_ROLE}" from public`);
|
|
204
|
-
await admin.unsafe(`grant usage on schema "${POOLER_ROLE}" to "${POOLER_ROLE}"`);
|
|
205
|
-
await admin.unsafe(`
|
|
206
|
-
create or replace function "${POOLER_ROLE}".get_auth(p_usename text)
|
|
207
|
-
returns table(usename name, passwd text)
|
|
208
|
-
language sql security definer set search_path = pg_catalog as $$
|
|
209
|
-
select usename, passwd from pg_shadow
|
|
210
|
-
where usename = p_usename and usename like 'app_%' and not usesuper
|
|
211
|
-
$$`);
|
|
212
|
-
await admin.unsafe(`revoke all on function "${POOLER_ROLE}".get_auth(text) from public`);
|
|
213
|
-
await admin.unsafe(`grant execute on function "${POOLER_ROLE}".get_auth(text) to "${POOLER_ROLE}"`);
|
|
214
|
-
} finally {
|
|
215
|
-
await admin.end();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/** Drops the app's schema (all its data) and role. */
|
|
219
|
-
async function dropDatabase({ app, adminUrl }) {
|
|
220
|
-
const role = dbRole(app);
|
|
221
|
-
const database = new URL(adminUrl).pathname.slice(1);
|
|
222
|
-
const admin = postgres(adminUrl, {
|
|
223
|
-
max: 1,
|
|
224
|
-
onnotice: () => {}
|
|
225
|
-
});
|
|
226
|
-
try {
|
|
227
|
-
await admin`select pg_terminate_backend(pid) from pg_stat_activity where usename = ${role}`;
|
|
228
|
-
await admin.unsafe(`drop schema if exists "${role}" cascade`);
|
|
229
|
-
const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
|
|
230
|
-
if (Boolean(row?.exists)) {
|
|
231
|
-
await admin.unsafe(`revoke all on database "${database}" from "${role}"`);
|
|
232
|
-
await admin.unsafe(`drop role "${role}"`);
|
|
233
|
-
}
|
|
234
|
-
} finally {
|
|
235
|
-
await admin.end();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
export { DEFAULT_ROLE_TIMEOUTS, MAX_KEY_BYTES, POOLER_ROLE, StorageError, appDatabaseUrl, createStorage, dbPassword, dbRole, dropDatabase, hmac, migrateAs, parseDuration, provisionDatabase, setRoleTimeouts, setupPooler, storageCredentials };
|
|
1
|
+
export * from "@wervt/platform";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineNuxtPlugin, useRouter, useRuntimeConfig } from "#app";
|
|
2
|
+
import { trackPageView } from "@wervt/client";
|
|
2
3
|
// src/runtime/app/plugins/analytics.client.ts
|
|
3
4
|
var analytics_client_default = defineNuxtPlugin({
|
|
4
5
|
name: "wervt:analytics",
|
|
@@ -7,8 +8,7 @@ var analytics_client_default = defineNuxtPlugin({
|
|
|
7
8
|
const base = useRuntimeConfig().app.baseURL.replace(/\/$/, "");
|
|
8
9
|
router.afterEach((to, from, failure) => {
|
|
9
10
|
if (failure || !from.matched.length || to.path === from.path) return;
|
|
10
|
-
|
|
11
|
-
navigator.sendBeacon?.(`${base}/_wervt/view`, body);
|
|
11
|
+
trackPageView(`${base}${to.path}`, { base });
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
});
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
import { pendingFeedbackContext } from "../useWervtFeedback.js";
|
|
2
1
|
import { defineNuxtPlugin, onNuxtReady, useRuntimeConfig } from "#app";
|
|
2
|
+
import { loadFeedback } from "@wervt/client";
|
|
3
3
|
// src/runtime/app/plugins/feedback.client.ts
|
|
4
4
|
var feedback_client_default = defineNuxtPlugin({
|
|
5
5
|
name: "wervt:feedback",
|
|
6
6
|
setup() {
|
|
7
7
|
const config = useRuntimeConfig();
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"label",
|
|
19
|
-
"color"
|
|
20
|
-
]) if (feedback[name]) script.dataset[name] = feedback[name];
|
|
21
|
-
script.dataset.context = JSON.stringify({
|
|
22
|
-
app: feedback.app,
|
|
23
|
-
release: config.app.buildId,
|
|
24
|
-
...pendingFeedbackContext
|
|
25
|
-
});
|
|
26
|
-
script.addEventListener("load", () => window.kontor?.setContext(pendingFeedbackContext));
|
|
27
|
-
document.head.appendChild(script);
|
|
28
|
-
});
|
|
8
|
+
const options = config.public.wervt?.feedback;
|
|
9
|
+
if (!options?.key) return;
|
|
10
|
+
const { app, ...widget } = options;
|
|
11
|
+
onNuxtReady(() => loadFeedback({
|
|
12
|
+
...widget,
|
|
13
|
+
context: {
|
|
14
|
+
app,
|
|
15
|
+
release: config.app.buildId
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
29
18
|
}
|
|
30
19
|
});
|
|
31
20
|
export { feedback_client_default as default };
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { useWervtUser } from "../useWervtUser.js";
|
|
2
2
|
import { defineNuxtPlugin } from "#app";
|
|
3
|
+
import { fetchUser } from "@wervt/client";
|
|
3
4
|
// src/runtime/app/plugins/user.client.ts
|
|
4
5
|
var user_client_default = defineNuxtPlugin({
|
|
5
6
|
name: "wervt:user",
|
|
6
7
|
async setup(nuxtApp) {
|
|
7
8
|
if (nuxtApp.payload.serverRendered) return;
|
|
8
|
-
|
|
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
|
-
}
|
|
9
|
+
useWervtUser().value = await fetchUser();
|
|
15
10
|
}
|
|
16
11
|
});
|
|
17
12
|
export { user_client_default as default };
|