@wervt/nuxt 0.3.9 → 0.3.10

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.
Files changed (58) hide show
  1. package/dist/brand.d.ts +10 -3
  2. package/dist/brand.js +19 -5
  3. package/dist/module.d.ts +3 -14
  4. package/dist/module.js +22 -59
  5. package/dist/provision.d.ts +1 -77
  6. package/dist/provision.js +1 -238
  7. package/dist/runtime/app/plugins/analytics.client.js +2 -2
  8. package/dist/runtime/app/plugins/feedback.client.js +11 -22
  9. package/dist/runtime/app/plugins/user.client.js +2 -7
  10. package/dist/runtime/app/plugins/webmcp.client.js +4 -41
  11. package/dist/runtime/app/snapshot.d.ts +2 -15
  12. package/dist/runtime/app/snapshot.js +1 -63
  13. package/dist/runtime/app/useShape.js +1 -1
  14. package/dist/runtime/app/useWervtFeedback.d.ts +1 -13
  15. package/dist/runtime/app/useWervtFeedback.js +3 -11
  16. package/dist/runtime/app/useWervtTrack.js +4 -8
  17. package/dist/runtime/app/useWervtUser.d.ts +3 -2
  18. package/dist/runtime/app/useWervtUser.js +3 -2
  19. package/dist/runtime/server/access.d.ts +3 -22
  20. package/dist/runtime/server/access.js +7 -24
  21. package/dist/runtime/server/anthropic.d.ts +2 -7
  22. package/dist/runtime/server/anthropic.js +6 -37
  23. package/dist/runtime/server/db.d.ts +2 -17
  24. package/dist/runtime/server/db.js +1 -35
  25. package/dist/runtime/server/errors.d.ts +6 -0
  26. package/dist/runtime/server/errors.js +21 -0
  27. package/dist/runtime/server/identity.d.ts +3 -9
  28. package/dist/runtime/server/identity.js +4 -23
  29. package/dist/runtime/server/index.d.ts +1 -1
  30. package/dist/runtime/server/index.js +1 -1
  31. package/dist/runtime/server/mail.js +11 -29
  32. package/dist/runtime/server/mcp-claims.d.ts +2 -3
  33. package/dist/runtime/server/mcp-claims.js +1 -17
  34. package/dist/runtime/server/seed.d.ts +2 -16
  35. package/dist/runtime/server/seed.js +1 -26
  36. package/dist/runtime/server/shape.d.ts +2 -2
  37. package/dist/runtime/server/shape.js +5 -45
  38. package/dist/runtime/server/task-handlers.d.ts +1 -5
  39. package/dist/runtime/server/task-handlers.js +3 -9
  40. package/dist/runtime/server/task-run-nitro2.d.ts +1 -5
  41. package/dist/runtime/server/task-run-nitro3.d.ts +1 -5
  42. package/dist/runtime/server/tasks.d.ts +2 -32
  43. package/dist/runtime/server/tasks.js +1 -9
  44. package/dist/runtime/server/user-route.d.ts +1 -3
  45. package/dist/runtime/server/user.js +8 -11
  46. package/dist/runtime/shared/agent-token.d.ts +2 -7
  47. package/dist/runtime/shared/agent-token.js +1 -16
  48. package/dist/runtime/shared/shape.d.ts +2 -17
  49. package/dist/runtime/shared/shape.js +1 -15
  50. package/dist/runtime/shared/user.d.ts +3 -19
  51. package/dist/runtime/shared/user.js +1 -9
  52. package/package.json +5 -1
  53. package/dist/dev.d.ts +0 -31
  54. package/dist/dev.js +0 -158
  55. package/dist/jsdoc-imports.js +0 -25
  56. package/dist/seed.js +0 -51
  57. package/dist/storage.d.ts +0 -72
  58. package/dist/storage.js +0 -362
package/dist/brand.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export interface BrandOptions {
2
- /** The product name the login shows ("Sign in to …"). Defaults to the app's `app.head.title`. */
2
+ /**
3
+ * The product name the login shows ("Sign in to …"). Defaults to the app's `app.head.title` up
4
+ * to its first separator, so "Lightdrop · Drop it. Get it." gives "Lightdrop".
5
+ */
3
6
  name?: string;
4
7
  /**
5
8
  * A wide logo for the top of the login page: a file in `public/` or an https URL. Without one,
@@ -8,10 +11,14 @@ export interface BrandOptions {
8
11
  logo?: string;
9
12
  /**
10
13
  * 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 `<link rel="icon">`.
14
+ * mail, since Gmail and Outlook don't show SVG. Defaults to the app's PNG icon in `app.head`
15
+ * (`<link rel="icon">` or `apple-touch-icon`), else its first `<link rel="icon">`.
12
16
  */
13
17
  icon?: string;
14
- /** The login's primary color as hex (`#ff5a1f`). Defaults to the `theme-color` meta tag. */
18
+ /**
19
+ * The login's primary color as hex (`#ff5a1f`). Without it the login keeps wervt's: the
20
+ * `theme-color` meta tag is usually the page background, not an accent.
21
+ */
15
22
  color?: string;
16
23
  /**
17
24
  * The small "Secured by wervt" under the login and in the mail footer. On by default: it says
package/dist/brand.js CHANGED
@@ -1,4 +1,4 @@
1
- import { headIcon, headMeta } from "./preview.js";
1
+ import { headIcon } from "./preview.js";
2
2
  // src/brand.ts
3
3
  const SIGN_IN_METHODS = [
4
4
  "google",
@@ -16,21 +16,35 @@ function signInMethods(value) {
16
16
  return methods.length ? methods : void 0;
17
17
  }
18
18
  const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
19
+ /** Image types Gmail and Outlook show; SVG and ICO stay blank there. */
20
+ const MAIL_IMAGE = /\.(png|jpe?g|gif)(\?.*)?$/i;
21
+ const TITLE_SEPARATOR = /\s+[·|–—-]\s+|\s*[·|–—]\s*|:\s+/;
22
+ /** The product name in a page title, without its tagline. */
23
+ function titleName(title) {
24
+ return title.split(TITLE_SEPARATOR)[0].trim() || title.trim();
25
+ }
26
+ function mailIcon(head) {
27
+ return (head.link?.find((link) => [
28
+ "icon",
29
+ "shortcut icon",
30
+ "apple-touch-icon"
31
+ ].includes(String(link.rel)) && typeof link.href === "string" && MAIL_IMAGE.test(link.href)))?.href;
32
+ }
19
33
  /** The login's branding for the app, or undefined while the app keeps wervt's. */
20
34
  function brandManifest(option, head, app, favicon) {
21
35
  if (!option) return void 0;
22
36
  const custom = option === true ? {} : option;
23
37
  const pick = (value, fallback) => value?.trim() || fallback;
24
- const title = typeof head.title === "string" && head.title.trim() ? head.title.trim() : app;
25
- const color = pick(custom.color, headMeta(head, "theme-color"));
38
+ const title = typeof head.title === "string" && head.title.trim() ? titleName(head.title) : app;
39
+ const color = custom.color?.trim();
26
40
  const signIn = signInMethods(custom.signIn);
27
41
  return {
28
42
  name: pick(custom.name, title).slice(0, 60),
29
43
  logo: pick(custom.logo, void 0),
30
- icon: pick(custom.icon, headIcon(head) ?? favicon),
44
+ icon: pick(custom.icon, mailIcon(head) ?? headIcon(head) ?? favicon),
31
45
  color: color && HEX_COLOR.test(color) ? color : void 0,
32
46
  ...custom.securedByWervt === false && { securedByWervt: false },
33
47
  ...signIn && { signIn }
34
48
  };
35
49
  }
36
- export { HEX_COLOR, SIGN_IN_METHODS, brandManifest, signInMethods };
50
+ 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,20 @@
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 { SIGN_IN_METHODS, brandManifest } from "./brand.js";
7
+ import { MAIL_IMAGE, SIGN_IN_METHODS, brandManifest } from "./brand.js";
11
8
  import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
9
+ import { BUNDLE_MANIFEST } from "@wervt/core";
10
+ import { SEED_TASK, resolveAppName, setupDev, unscheduleSeed } from "@wervt/platform/dev";
12
11
  import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, updateTemplates, useLogger } from "nuxt/kit";
13
12
  import { existsSync } from "node:fs";
14
13
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
15
- import { builtinModules, createRequire } from "node:module";
14
+ import { createRequire } from "node:module";
16
15
  import { dirname, extname, join, relative, resolve, sep } from "node:path";
17
16
  import evlog from "evlog/nuxt";
17
+ import { DENO_INJECT, denoPlugins } from "@wervt/nitro/build";
18
18
  // src/module.ts
19
19
  const CONTENT_DATABASE = {
20
20
  type: "postgres",
@@ -86,7 +86,7 @@ var module_default = defineNuxtModule({
86
86
  inline: [
87
87
  ...options.externals?.inline ?? [],
88
88
  resolver.resolve("./runtime"),
89
- /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
89
+ /(^|\/node_modules\/)(@wervt\/(core|client)|drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
90
90
  ]
91
91
  };
92
92
  const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
@@ -106,18 +106,9 @@ var module_default = defineNuxtModule({
106
106
  }
107
107
  if (!nuxt.options.dev) options.rollupConfig = {
108
108
  ...options.rollupConfig,
109
- plugins: [
110
- ...[options.rollupConfig?.plugins ?? []].flat(),
111
- prefixNodeBuiltins(),
112
- configurableWaitUntil(),
113
- stripJsdocImports()
114
- ]
109
+ plugins: [...[options.rollupConfig?.plugins ?? []].flat(), ...denoPlugins()]
115
110
  };
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
- } }];
111
+ options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject: DENO_INJECT }];
121
112
  });
122
113
  if (nitroMajor >= 3 && !nuxt.options.dev) nuxt.hook("vite:extendConfig", (config, { isServer }) => {
123
114
  const ssr = config.environments?.ssr;
@@ -202,6 +193,10 @@ var module_default = defineNuxtModule({
202
193
  };
203
194
  await mkdir(nitroOptions.output.serverDir, { recursive: true });
204
195
  await writeFile(join(nitroOptions.output.serverDir, "wervt-tasks.json"), `${JSON.stringify(manifest, null, 2)}\n`);
196
+ await writeFile(join(dirname(nitroOptions.output.serverDir), BUNDLE_MANIFEST), `${JSON.stringify({
197
+ version: 2,
198
+ framework: "nuxt"
199
+ }, null, 2)}\n`);
205
200
  });
206
201
  });
207
202
  }
@@ -282,7 +277,15 @@ var module_default = defineNuxtModule({
282
277
  if (nuxt.options.dev && process.env.WERVT_PREVIEW) setupStudioPreview(nuxt);
283
278
  if (nuxt.options.dev && !nuxt.options._prepare) {
284
279
  const app = await resolveAppName(nuxt.options.rootDir, options.app);
285
- await setupDev(nuxt, app, options.dev ?? {}, { storage: useAblage });
280
+ await setupDev({
281
+ rootDir: nuxt.options.rootDir,
282
+ onClose: (fn) => nuxt.hook("close", fn),
283
+ restart: () => void nuxt.callHook("restart"),
284
+ onListen: (fn) => nuxt.hook("listen", (_server, listener) => {
285
+ if (listener.url) fn(listener.url);
286
+ }),
287
+ logger
288
+ }, app, options.dev ?? {}, { storage: useAblage });
286
289
  }
287
290
  addTypeTemplate({
288
291
  filename: "types/wervt-email.d.ts",
@@ -397,6 +400,7 @@ var module_default = defineNuxtModule({
397
400
  icon: brand.icon,
398
401
  image: brand.logo
399
402
  } : false)) if (!existsSync(join(publicDir, file))) logger.warn(`The login branding names \`${file}\`, which isn't in public/.`);
403
+ 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
404
  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.");
401
405
  if (typeof options.brand === "object" && options.brand.signIn) {
402
406
  const unknown = options.brand.signIn.filter((m) => !SIGN_IN_METHODS.includes(m));
@@ -506,47 +510,6 @@ function hasPackage(dir, name) {
506
510
  }
507
511
  /** evlog's Nitro 2 plugin and error handler, as evlog/nuxt resolves them. */
508
512
  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
513
  /** `pattern`, except for files of this module's runtime. */
551
514
  function exceptRuntime(pattern) {
552
515
  if (pattern.source.startsWith("^")) return pattern;
@@ -1,77 +1 @@
1
- import { MAX_KEY_BYTES, ObjectDetails, ObjectListing, StorageError, StorageOptions, StorageQuota, StorageUsage, StoredObject, createStorage } from "./storage.js";
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
- import { MAX_KEY_BYTES, StorageError, createStorage } from "./storage.js";
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
- const body = new Blob([JSON.stringify({ path: `${base}${to.path}` })], { type: "application/json" });
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 feedback = config.public.wervt?.feedback;
9
- if (!feedback?.key) return;
10
- onNuxtReady(() => {
11
- const script = document.createElement("script");
12
- script.src = `${feedback.url.replace(/\/$/, "")}/widget.js`;
13
- script.async = true;
14
- script.dataset.key = feedback.key;
15
- for (const name of [
16
- "lang",
17
- "position",
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
- 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
- }
9
+ useWervtUser().value = await fetchUser();
15
10
  }
16
11
  });
17
12
  export { user_client_default as default };