@wervt/nuxt 0.3.1 → 0.3.2
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 +2 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.js +20 -0
- package/dist/preview.d.ts +14 -0
- package/dist/preview.js +39 -0
- package/dist/runtime/server/access.d.ts +34 -0
- package/dist/runtime/server/access.js +87 -0
- package/dist/runtime/server/index.d.ts +2 -1
- package/dist/runtime/server/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ export default defineNuxtConfig({
|
|
|
18
18
|
- `useDb()` (server): Drizzle on the app's own schema, with migrations from `drizzle/`.
|
|
19
19
|
- `proxyShape()` (server) and `useShape()` (app): live rows from Postgres via Electric.
|
|
20
20
|
- `useWervtUser()`: the signed-in wervt user, on the server and in the app.
|
|
21
|
+
- `useWervtAccess(event)`: invites on invite-only apps (`wervt access <app> --invite`), as the
|
|
22
|
+
signed-in user.
|
|
21
23
|
- `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
|
|
22
24
|
- `useWervtTrack()`: custom events for wervt's cookieless analytics (page views are counted automatically).
|
|
23
25
|
- `wervt: { feedback: { key } }` and `useWervtFeedback()`: the kontor feedback widget, reports go into a kontor project's triage.
|
package/dist/module.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DevOptions } from "./dev.js";
|
|
2
|
+
import { PreviewOptions } from "./preview.js";
|
|
2
3
|
//#region src/module.d.ts
|
|
3
4
|
export interface ModuleOptions {
|
|
4
5
|
/** App name on wervt. Defaults to the package.json name. */
|
|
@@ -37,6 +38,13 @@ export interface ModuleOptions {
|
|
|
37
38
|
* (it can override a configured key the same way).
|
|
38
39
|
*/
|
|
39
40
|
feedback?: FeedbackOptions | boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The link preview of a private app, shown without a login when its link is shared in chats.
|
|
43
|
+
* Defaults to the app's name and icon only; `true` takes title, description, `og:image` and
|
|
44
|
+
* theme color from `app.head`, an object sets them (falling back to `app.head`). `false`
|
|
45
|
+
* sends visitors without a login straight to the login, with no preview of the app.
|
|
46
|
+
*/
|
|
47
|
+
preview?: PreviewOptions | boolean;
|
|
40
48
|
}
|
|
41
49
|
export interface FeedbackOptions {
|
|
42
50
|
/** Public intake key of the kontor project (Project settings → Intake keys). */
|
package/dist/module.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SEED_TASK, unscheduleSeed } from "./seed.js";
|
|
2
2
|
import { resolveAppName, setupDev } from "./dev.js";
|
|
3
3
|
import { findRouteTools } from "./route-tools.js";
|
|
4
|
+
import { localFiles, previewManifest } from "./preview.js";
|
|
4
5
|
import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
|
|
5
6
|
import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, updateTemplates, useLogger } from "nuxt/kit";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
@@ -341,6 +342,20 @@ var module_default = defineNuxtModule({
|
|
|
341
342
|
await writeFile(join(serverDir, "wervt-shapes.json"), `${JSON.stringify(shapesManifest(await findShapeRoutes(shapesDir)), null, 2)}\n`);
|
|
342
343
|
});
|
|
343
344
|
});
|
|
345
|
+
if (!nuxt.options.dev) {
|
|
346
|
+
const app = await resolveAppName(nuxt.options.rootDir, options.app);
|
|
347
|
+
const publicDir = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
|
|
348
|
+
const favicon = FAVICONS.find((file) => existsSync(join(publicDir, file)));
|
|
349
|
+
const manifest = previewManifest(options.preview, nuxt.options.app.head, app, favicon && `/${favicon}`);
|
|
350
|
+
for (const file of localFiles(manifest)) if (!existsSync(join(publicDir, file))) logger.warn(`The link preview names \`${file}\`, which isn't in public/.`);
|
|
351
|
+
nuxt.hook("nitro:init", (instance) => {
|
|
352
|
+
const { serverDir } = instance.options.output;
|
|
353
|
+
instance.hooks.hook("compiled", async () => {
|
|
354
|
+
await mkdir(serverDir, { recursive: true });
|
|
355
|
+
await writeFile(join(serverDir, "wervt-preview.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
344
359
|
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
|
|
345
360
|
addImports([
|
|
346
361
|
{
|
|
@@ -378,6 +393,11 @@ var module_default = defineNuxtModule({
|
|
|
378
393
|
]);
|
|
379
394
|
}
|
|
380
395
|
});
|
|
396
|
+
const FAVICONS = [
|
|
397
|
+
"favicon.png",
|
|
398
|
+
"favicon.svg",
|
|
399
|
+
"favicon.ico"
|
|
400
|
+
];
|
|
381
401
|
const TOOL_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
382
402
|
".ts",
|
|
383
403
|
".js",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/preview.d.ts
|
|
2
|
+
export interface PreviewOptions {
|
|
3
|
+
/** Defaults to the app's `app.head.title`. */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Defaults to the app's `description` or `og:description` meta tag. */
|
|
6
|
+
description?: string;
|
|
7
|
+
/** A file in `public/` (`/og.png`, ideally 1200×630) or an https URL. Defaults to `og:image`. */
|
|
8
|
+
image?: string;
|
|
9
|
+
/** A file in `public/` or an https URL. Defaults to the app's `<link rel="icon">` or favicon. */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Defaults to the app's `theme-color` meta tag. */
|
|
12
|
+
themeColor?: string;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/preview.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the runtime shows. Without `preview`, only the app's name and icon: for some private apps
|
|
4
|
+
* the title alone says too much, so the rest of the head is only used once the app opts in.
|
|
5
|
+
*/
|
|
6
|
+
function previewManifest(option, head, app, favicon) {
|
|
7
|
+
if (option === false) return false;
|
|
8
|
+
const icon = headIcon(head) ?? favicon;
|
|
9
|
+
if (option === void 0) return {
|
|
10
|
+
title: app,
|
|
11
|
+
icon
|
|
12
|
+
};
|
|
13
|
+
const fromHead = {
|
|
14
|
+
title: typeof head.title === "string" && head.title ? head.title : app,
|
|
15
|
+
description: headMeta(head, "description") ?? headMeta(head, "og:description"),
|
|
16
|
+
image: headMeta(head, "og:image"),
|
|
17
|
+
icon,
|
|
18
|
+
themeColor: headMeta(head, "theme-color")
|
|
19
|
+
};
|
|
20
|
+
if (option === true) return fromHead;
|
|
21
|
+
const manifest = { ...fromHead };
|
|
22
|
+
for (const [key, value] of Object.entries(option)) if (typeof value === "string" && value) manifest[key] = value;
|
|
23
|
+
return manifest;
|
|
24
|
+
}
|
|
25
|
+
function headMeta(head, key) {
|
|
26
|
+
const tag = head.meta?.find((meta) => meta.name === key || meta.property === key);
|
|
27
|
+
return typeof tag?.content === "string" && tag.content ? tag.content : void 0;
|
|
28
|
+
}
|
|
29
|
+
function headIcon(head) {
|
|
30
|
+
const link = head.link?.find((link) => link.rel === "icon" || link.rel === "shortcut icon");
|
|
31
|
+
return typeof link?.href === "string" && link.href ? link.href : void 0;
|
|
32
|
+
}
|
|
33
|
+
/** Local files the manifest names (not URLs), to check they're in `public/`. */
|
|
34
|
+
function localFiles(manifest) {
|
|
35
|
+
if (!manifest) return [];
|
|
36
|
+
return [manifest.image, manifest.icon].filter((path) => !!path && path.startsWith("/") && !path.startsWith("//"));
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { localFiles, previewManifest };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RequestEvent } from "nuxt/server";
|
|
2
|
+
//#region src/runtime/server/access.d.ts
|
|
3
|
+
export interface WervtInvite {
|
|
4
|
+
email: string;
|
|
5
|
+
/** Who invited them. */
|
|
6
|
+
by: string;
|
|
7
|
+
/** ISO time. */
|
|
8
|
+
at: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WervtAccess {
|
|
11
|
+
/** Lets `email` into the app. `invited` is false when they could already get in. */
|
|
12
|
+
invite(email: string): Promise<{
|
|
13
|
+
email: string;
|
|
14
|
+
invited: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
/** Takes back an invite (not the owner's allow list). */
|
|
17
|
+
revoke(email: string): Promise<{
|
|
18
|
+
email: string;
|
|
19
|
+
revoked: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/** Everyone the app invited, with who invited them and when. */
|
|
22
|
+
list(): Promise<WervtInvite[]>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Invites on an invite-only app (`wervt access <app> --invite`), as the user of this request: the
|
|
26
|
+
* runtime vouches for them, and only someone with access can invite. Everyone with access may
|
|
27
|
+
* invite; restrict it in the route if the app has its own roles.
|
|
28
|
+
*
|
|
29
|
+
* Throws with the platform's status: 401 without a signed-in user, 403 when they have no access,
|
|
30
|
+
* 409 when the app isn't invite-only, 429 past the limits (25 invites per person and day, 500 per
|
|
31
|
+
* app).
|
|
32
|
+
*/
|
|
33
|
+
export declare function useWervtAccess(event: RequestEvent): WervtAccess;
|
|
34
|
+
//#endregion
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DEV_LOGIN_COOKIE, devUser } from "../shared/user.js";
|
|
2
|
+
import { createError, getCookie } from "nuxt/server";
|
|
3
|
+
//#region src/runtime/server/access.ts
|
|
4
|
+
const devInvites = [];
|
|
5
|
+
/**
|
|
6
|
+
* Invites on an invite-only app (`wervt access <app> --invite`), as the user of this request: the
|
|
7
|
+
* runtime vouches for them, and only someone with access can invite. Everyone with access may
|
|
8
|
+
* invite; restrict it in the route if the app has its own roles.
|
|
9
|
+
*
|
|
10
|
+
* Throws with the platform's status: 401 without a signed-in user, 403 when they have no access,
|
|
11
|
+
* 409 when the app isn't invite-only, 429 past the limits (25 invites per person and day, 500 per
|
|
12
|
+
* app).
|
|
13
|
+
*/
|
|
14
|
+
function useWervtAccess(event) {
|
|
15
|
+
const runtime = process.env.WERVT_RUNTIME_URL;
|
|
16
|
+
if (!runtime) return devAccess(event);
|
|
17
|
+
async function call(method, body) {
|
|
18
|
+
const res = await fetch(new URL("/_wervt/access", runtime), {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
"x-wervt-app": process.env.WERVT_APP ?? "",
|
|
23
|
+
"x-wervt-token": process.env.WERVT_TOKEN ?? "",
|
|
24
|
+
"x-wervt-user": event.req.headers.get("x-wervt-user") ?? "",
|
|
25
|
+
"x-wervt-user-proof": event.req.headers.get("x-wervt-user-proof") ?? ""
|
|
26
|
+
},
|
|
27
|
+
body: body && JSON.stringify(body)
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json().catch(() => ({}));
|
|
30
|
+
if (!res.ok) throw createError({
|
|
31
|
+
status: res.status,
|
|
32
|
+
message: data.error ?? `access ${res.status}`
|
|
33
|
+
});
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
invite: (email) => call("POST", { email }),
|
|
38
|
+
revoke: (email) => call("DELETE", { email }),
|
|
39
|
+
list: async () => (await call("GET")).invited
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function devAccess(event) {
|
|
43
|
+
const user = devUser(getCookie(event, DEV_LOGIN_COOKIE) === "1");
|
|
44
|
+
const by = () => {
|
|
45
|
+
if (!user) throw createError({
|
|
46
|
+
status: 401,
|
|
47
|
+
message: "sign in at /_wervt/login first"
|
|
48
|
+
});
|
|
49
|
+
return user.email;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
async invite(email) {
|
|
53
|
+
const inviter = by();
|
|
54
|
+
email = email.trim().toLowerCase();
|
|
55
|
+
if (devInvites.some((invite) => invite.email === email)) return {
|
|
56
|
+
email,
|
|
57
|
+
invited: false
|
|
58
|
+
};
|
|
59
|
+
devInvites.push({
|
|
60
|
+
email,
|
|
61
|
+
by: inviter,
|
|
62
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
63
|
+
});
|
|
64
|
+
console.info(`[access] ${inviter} invited ${email} (dev: not stored)`);
|
|
65
|
+
return {
|
|
66
|
+
email,
|
|
67
|
+
invited: true
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
async revoke(email) {
|
|
71
|
+
by();
|
|
72
|
+
email = email.trim().toLowerCase();
|
|
73
|
+
const index = devInvites.findIndex((invite) => invite.email === email);
|
|
74
|
+
if (index >= 0) devInvites.splice(index, 1);
|
|
75
|
+
return {
|
|
76
|
+
email,
|
|
77
|
+
revoked: index >= 0
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
async list() {
|
|
81
|
+
by();
|
|
82
|
+
return [...devInvites];
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { useWervtAccess };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WervtUser } from "../shared/user.js";
|
|
2
2
|
import { EmailProps, RenderedEmail, renderEmail } from "../email/render.js";
|
|
3
|
+
import { WervtAccess, WervtInvite, useWervtAccess } from "./access.js";
|
|
3
4
|
import { useDb } from "./db.js";
|
|
4
5
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
5
6
|
import { SendMailOptions, sendMail } from "./mail.js";
|
|
@@ -11,4 +12,4 @@ import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
|
11
12
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
12
13
|
import { Tool, ToolRoute, defineTool, defineToolRoute } from "./tools.js";
|
|
13
14
|
import { useWervtUser } from "./user.js";
|
|
14
|
-
export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type ToolRoute, type WervtUser, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
15
|
+
export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type ToolRoute, type WervtAccess, type WervtInvite, type WervtUser, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, vh };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useWervtAccess } from "./access.js";
|
|
2
2
|
import { useDb } from "./db.js";
|
|
3
|
+
import { renderEmail } from "../email/render.js";
|
|
3
4
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
4
5
|
import { sendMail } from "./mail.js";
|
|
5
6
|
import { seedTable } from "./seed.js";
|
|
@@ -8,4 +9,4 @@ import { defineTask } from "./tasks.js";
|
|
|
8
9
|
import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
9
10
|
import { defineTool, defineToolRoute } from "./tools.js";
|
|
10
11
|
import { useWervtUser } from "./user.js";
|
|
11
|
-
export { createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
12
|
+
export { createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, vh };
|