@wervt/nuxt 0.3.5 → 0.3.7
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 +21 -0
- package/dist/brand.js +19 -0
- package/dist/dev.js +16 -2
- package/dist/jsdoc-imports.js +25 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.js +11 -1
- package/dist/preview.js +1 -1
- package/dist/runtime/email/alert.d.ts +98 -0
- package/dist/runtime/email/alert.js +114 -0
- package/dist/runtime/email/components.d.ts +24 -0
- package/dist/runtime/email/components.js +68 -12
- package/dist/runtime/email/index.d.ts +3 -2
- package/dist/runtime/email/index.js +2 -1
- package/dist/runtime/server/index.d.ts +2 -2
- package/dist/runtime/server/mail.d.ts +5 -0
- package/dist/runtime/server/mail.js +1 -0
- package/dist/runtime/server/task-handlers.d.ts +1 -1
- package/dist/runtime/server/tasks.d.ts +14 -0
- package/dist/runtime/server/tool-handlers.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +3 -4
- package/package.json +1 -1
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface BrandOptions {
|
|
2
|
+
/** The product name the login shows ("Sign in to …"). Defaults to the app's `app.head.title`. */
|
|
3
|
+
name?: string;
|
|
4
|
+
/**
|
|
5
|
+
* A wide logo for the top of the login page: a file in `public/` or an https URL. Without one,
|
|
6
|
+
* the login shows the icon next to the name.
|
|
7
|
+
*/
|
|
8
|
+
logo?: string;
|
|
9
|
+
/**
|
|
10
|
+
* 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">`.
|
|
12
|
+
*/
|
|
13
|
+
icon?: string;
|
|
14
|
+
/** The login's primary color as hex (`#ff5a1f`). Defaults to the `theme-color` meta tag. */
|
|
15
|
+
color?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The small "Secured by wervt" under the login and in the mail footer. On by default: it says
|
|
18
|
+
* why the address bar shows wervt and that the account works in other apps too.
|
|
19
|
+
*/
|
|
20
|
+
securedByWervt?: boolean;
|
|
21
|
+
}
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { headIcon, headMeta } from "./preview.js";
|
|
2
|
+
// src/brand.ts
|
|
3
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
4
|
+
/** The login's branding for the app, or undefined while the app keeps wervt's. */
|
|
5
|
+
function brandManifest(option, head, app, favicon) {
|
|
6
|
+
if (!option) return void 0;
|
|
7
|
+
const custom = option === true ? {} : option;
|
|
8
|
+
const pick = (value, fallback) => value?.trim() || fallback;
|
|
9
|
+
const title = typeof head.title === "string" && head.title.trim() ? head.title.trim() : app;
|
|
10
|
+
const color = pick(custom.color, headMeta(head, "theme-color"));
|
|
11
|
+
return {
|
|
12
|
+
name: pick(custom.name, title).slice(0, 60),
|
|
13
|
+
logo: pick(custom.logo, void 0),
|
|
14
|
+
icon: pick(custom.icon, headIcon(head) ?? favicon),
|
|
15
|
+
color: color && HEX_COLOR.test(color) ? color : void 0,
|
|
16
|
+
...custom.securedByWervt === false && { securedByWervt: false }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export { HEX_COLOR, brandManifest };
|
package/dist/dev.js
CHANGED
|
@@ -37,7 +37,16 @@ async function setupDev(nuxt, app, options, features = {}) {
|
|
|
37
37
|
});
|
|
38
38
|
if (features.storage) await setupStorage(app, config.dbSecret, options);
|
|
39
39
|
const migrationsFolder = join(nuxt.options.rootDir, "drizzle");
|
|
40
|
-
if (!existsSync(migrationsFolder))
|
|
40
|
+
if (!existsSync(migrationsFolder)) {
|
|
41
|
+
const watcher = watch(nuxt.options.rootDir, (_event, file) => {
|
|
42
|
+
if (file !== "drizzle" || !existsSync(migrationsFolder)) return;
|
|
43
|
+
watcher.close();
|
|
44
|
+
logger.info("Migrations added, restarting…");
|
|
45
|
+
nuxt.callHook("restart");
|
|
46
|
+
});
|
|
47
|
+
nuxt.hook("close", () => watcher.close());
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
41
50
|
const provision = {
|
|
42
51
|
app,
|
|
43
52
|
adminUrl: config.adminUrl,
|
|
@@ -133,8 +142,13 @@ async function setupStorage(app, secret, options) {
|
|
|
133
142
|
process.env.WERVT_S3_ACCESS_KEY_ID ??= creds.accessKeyId;
|
|
134
143
|
process.env.WERVT_S3_SECRET_ACCESS_KEY ??= creds.secretAccessKey;
|
|
135
144
|
}
|
|
136
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* App name, normalised to `[a-z0-9-]+`: `WERVT_APP` first, which `wervt deploy` sets for its
|
|
147
|
+
* build, so one codebase deployed as several apps (`--name`) builds each under its own name.
|
|
148
|
+
* Then the module option, then package.json.
|
|
149
|
+
*/
|
|
137
150
|
async function resolveAppName(rootDir, name) {
|
|
151
|
+
name = process.env.WERVT_APP || name;
|
|
138
152
|
if (!name) {
|
|
139
153
|
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8").catch(() => "{}"));
|
|
140
154
|
name = String(pkg.name ?? "app").replace(/^@[^/]+\//, "");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/jsdoc-imports.ts
|
|
2
|
+
/**
|
|
3
|
+
* Deno's module graph reads JSDoc type imports (`{@link import('../internal/headers').x}`) as
|
|
4
|
+
* real dependencies and refuses to boot when one can't be resolved. Packages ship such paths
|
|
5
|
+
* relative to their own source tree (@anthropic-ai/sdk does), so once Nitro bundles them into
|
|
6
|
+
* `server/_libs/…` the path points nowhere and the worker fails with "Module not found".
|
|
7
|
+
* Dropping the `import(` from JSDoc lines keeps the comment readable and out of the graph. Only
|
|
8
|
+
* lines that open a JSDoc block or continue one with `*` are touched, so code never changes.
|
|
9
|
+
*/
|
|
10
|
+
function stripJsdocImports() {
|
|
11
|
+
const JSDOC_LINE_RE = /^[ \t]*(?:\/\*\*|\*(?!\/)).*$/gm;
|
|
12
|
+
const IMPORT_RE = /\bimport\(\s*(['"])/g;
|
|
13
|
+
return {
|
|
14
|
+
name: "wervt:strip-jsdoc-imports",
|
|
15
|
+
renderChunk(code) {
|
|
16
|
+
if (!code.includes("import(")) return null;
|
|
17
|
+
const out = code.replace(JSDOC_LINE_RE, (line) => line.replace(IMPORT_RE, "$1"));
|
|
18
|
+
return out === code ? null : {
|
|
19
|
+
code: out,
|
|
20
|
+
map: null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export { stripJsdocImports };
|
package/dist/module.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DevOptions } from "./dev.js";
|
|
2
2
|
import { PreviewOptions } from "./preview.js";
|
|
3
|
+
import { BrandOptions } from "./brand.js";
|
|
3
4
|
export interface ModuleOptions {
|
|
4
5
|
/** App name on wervt. Defaults to the package.json name. */
|
|
5
6
|
app?: string;
|
|
@@ -44,6 +45,13 @@ export interface ModuleOptions {
|
|
|
44
45
|
* sends visitors without a login straight to the login, with no preview of the app.
|
|
45
46
|
*/
|
|
46
47
|
preview?: PreviewOptions | boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The login in this app's look: its name, logo, icon and color instead of wervt's, on the login
|
|
50
|
+
* and consent pages and in the sign-in code mail, with only a small "Secured by wervt" left.
|
|
51
|
+
* `true` takes name, icon and color from `app.head`; an object sets them (falling back to
|
|
52
|
+
* `app.head`). Off by default.
|
|
53
|
+
*/
|
|
54
|
+
brand?: BrandOptions | boolean;
|
|
47
55
|
}
|
|
48
56
|
export interface FeedbackOptions {
|
|
49
57
|
/** Public intake key of the kontor project (Project settings → Intake keys). */
|
package/dist/module.js
CHANGED
|
@@ -4,8 +4,10 @@ import { setupStudioPreview } from "./studio-preview.js";
|
|
|
4
4
|
import { PROCESS_ENV_TYPES } from "./env-types.js";
|
|
5
5
|
import { dependencyNames, ogImageDefaults, ogImageWarning } from "./og-image.js";
|
|
6
6
|
import { iconDefaults, iconSources, iconWarning } from "./icons.js";
|
|
7
|
+
import { stripJsdocImports } from "./jsdoc-imports.js";
|
|
7
8
|
import { findRouteTools } from "./route-tools.js";
|
|
8
9
|
import { localFiles, previewManifest } from "./preview.js";
|
|
10
|
+
import { brandManifest } from "./brand.js";
|
|
9
11
|
import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
|
|
10
12
|
import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, updateTemplates, useLogger } from "nuxt/kit";
|
|
11
13
|
import { existsSync } from "node:fs";
|
|
@@ -107,7 +109,8 @@ var module_default = defineNuxtModule({
|
|
|
107
109
|
plugins: [
|
|
108
110
|
...[options.rollupConfig?.plugins ?? []].flat(),
|
|
109
111
|
prefixNodeBuiltins(),
|
|
110
|
-
configurableWaitUntil()
|
|
112
|
+
configurableWaitUntil(),
|
|
113
|
+
stripJsdocImports()
|
|
111
114
|
]
|
|
112
115
|
};
|
|
113
116
|
options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject: {
|
|
@@ -389,11 +392,18 @@ var module_default = defineNuxtModule({
|
|
|
389
392
|
const favicon = FAVICONS.find((file) => existsSync(join(publicDir, file)));
|
|
390
393
|
const manifest = previewManifest(options.preview, nuxt.options.app.head, app, favicon && `/${favicon}`);
|
|
391
394
|
for (const file of localFiles(manifest)) if (!existsSync(join(publicDir, file))) logger.warn(`The link preview names \`${file}\`, which isn't in public/.`);
|
|
395
|
+
const brand = brandManifest(options.brand, nuxt.options.app.head, app, favicon && `/${favicon}`);
|
|
396
|
+
for (const file of localFiles(brand ? {
|
|
397
|
+
icon: brand.icon,
|
|
398
|
+
image: brand.logo
|
|
399
|
+
} : false)) if (!existsSync(join(publicDir, file))) logger.warn(`The login branding names \`${file}\`, which isn't in public/.`);
|
|
400
|
+
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.");
|
|
392
401
|
nuxt.hook("nitro:init", (instance) => {
|
|
393
402
|
const { serverDir } = instance.options.output;
|
|
394
403
|
instance.hooks.hook("compiled", async () => {
|
|
395
404
|
await mkdir(serverDir, { recursive: true });
|
|
396
405
|
await writeFile(join(serverDir, "wervt-preview.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
406
|
+
if (brand) await writeFile(join(serverDir, "wervt-brand.json"), `${JSON.stringify(brand, null, 2)}\n`);
|
|
397
407
|
});
|
|
398
408
|
});
|
|
399
409
|
}
|
package/dist/preview.js
CHANGED
|
@@ -35,4 +35,4 @@ function localFiles(manifest) {
|
|
|
35
35
|
if (!manifest) return [];
|
|
36
36
|
return [manifest.image, manifest.icon].filter((path) => !!path && path.startsWith("/") && !path.startsWith("//"));
|
|
37
37
|
}
|
|
38
|
-
export { localFiles, previewManifest };
|
|
38
|
+
export { headIcon, headMeta, localFiles, previewManifest };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PropType } from "vue";
|
|
2
|
+
/**
|
|
3
|
+
* A saved log query matched at least as often as its alert allows: what matched, how often, a
|
|
4
|
+
* few of the entries, and a link to the logs.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LogAlertEmail: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
7
|
+
/** The app whose logs matched; `all apps` for a query across apps. */
|
|
8
|
+
app: {
|
|
9
|
+
type: StringConstructor;
|
|
10
|
+
required: true;
|
|
11
|
+
};
|
|
12
|
+
/** The saved query's name. */
|
|
13
|
+
name: {
|
|
14
|
+
type: StringConstructor;
|
|
15
|
+
required: true;
|
|
16
|
+
};
|
|
17
|
+
/** Its LogsQL. */
|
|
18
|
+
query: {
|
|
19
|
+
type: StringConstructor;
|
|
20
|
+
required: true;
|
|
21
|
+
};
|
|
22
|
+
count: {
|
|
23
|
+
type: NumberConstructor;
|
|
24
|
+
required: true;
|
|
25
|
+
};
|
|
26
|
+
threshold: {
|
|
27
|
+
type: NumberConstructor;
|
|
28
|
+
required: true;
|
|
29
|
+
};
|
|
30
|
+
/** How far back it counted, e.g. `5m`. */
|
|
31
|
+
window: {
|
|
32
|
+
type: StringConstructor;
|
|
33
|
+
required: true;
|
|
34
|
+
};
|
|
35
|
+
/** How long it stays quiet now, e.g. `1h`. */
|
|
36
|
+
cooldown: {
|
|
37
|
+
type: StringConstructor;
|
|
38
|
+
required: true;
|
|
39
|
+
};
|
|
40
|
+
/** The logs, filtered to the query. */
|
|
41
|
+
url: {
|
|
42
|
+
type: StringConstructor;
|
|
43
|
+
required: true;
|
|
44
|
+
};
|
|
45
|
+
/** First lines of the newest matching entries. */
|
|
46
|
+
samples: {
|
|
47
|
+
type: PropType<string[]>;
|
|
48
|
+
default: () => never[];
|
|
49
|
+
};
|
|
50
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
53
|
+
/** The app whose logs matched; `all apps` for a query across apps. */
|
|
54
|
+
app: {
|
|
55
|
+
type: StringConstructor;
|
|
56
|
+
required: true;
|
|
57
|
+
};
|
|
58
|
+
/** The saved query's name. */
|
|
59
|
+
name: {
|
|
60
|
+
type: StringConstructor;
|
|
61
|
+
required: true;
|
|
62
|
+
};
|
|
63
|
+
/** Its LogsQL. */
|
|
64
|
+
query: {
|
|
65
|
+
type: StringConstructor;
|
|
66
|
+
required: true;
|
|
67
|
+
};
|
|
68
|
+
count: {
|
|
69
|
+
type: NumberConstructor;
|
|
70
|
+
required: true;
|
|
71
|
+
};
|
|
72
|
+
threshold: {
|
|
73
|
+
type: NumberConstructor;
|
|
74
|
+
required: true;
|
|
75
|
+
};
|
|
76
|
+
/** How far back it counted, e.g. `5m`. */
|
|
77
|
+
window: {
|
|
78
|
+
type: StringConstructor;
|
|
79
|
+
required: true;
|
|
80
|
+
};
|
|
81
|
+
/** How long it stays quiet now, e.g. `1h`. */
|
|
82
|
+
cooldown: {
|
|
83
|
+
type: StringConstructor;
|
|
84
|
+
required: true;
|
|
85
|
+
};
|
|
86
|
+
/** The logs, filtered to the query. */
|
|
87
|
+
url: {
|
|
88
|
+
type: StringConstructor;
|
|
89
|
+
required: true;
|
|
90
|
+
};
|
|
91
|
+
/** First lines of the newest matching entries. */
|
|
92
|
+
samples: {
|
|
93
|
+
type: PropType<string[]>;
|
|
94
|
+
default: () => never[];
|
|
95
|
+
};
|
|
96
|
+
}>> & Readonly<{}>, {
|
|
97
|
+
samples: string[];
|
|
98
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { EmailButton, EmailDivider, EmailHeading, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { defineComponent, h } from "vue";
|
|
3
|
+
// src/runtime/email/alert.ts
|
|
4
|
+
const code = (text) => h("code", { style: {
|
|
5
|
+
fontFamily: emailTheme.mono,
|
|
6
|
+
fontSize: "13px",
|
|
7
|
+
background: emailTheme.codeBackground,
|
|
8
|
+
border: `1px solid ${emailTheme.accentBorder}`,
|
|
9
|
+
borderRadius: "4px",
|
|
10
|
+
padding: "1px 5px"
|
|
11
|
+
} }, text);
|
|
12
|
+
/** `5m` → `5 minutes`. */
|
|
13
|
+
function spelledOut(duration) {
|
|
14
|
+
const match = duration.match(/^(\d+)([mhd])$/);
|
|
15
|
+
if (!match) return duration;
|
|
16
|
+
const amount = Number(match[1]);
|
|
17
|
+
const unit = {
|
|
18
|
+
m: "minute",
|
|
19
|
+
h: "hour",
|
|
20
|
+
d: "day"
|
|
21
|
+
}[match[2]];
|
|
22
|
+
return amount === 1 ? `1 ${unit}` : `${amount} ${unit}s`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A saved log query matched at least as often as its alert allows: what matched, how often, a
|
|
26
|
+
* few of the entries, and a link to the logs.
|
|
27
|
+
*/
|
|
28
|
+
const LogAlertEmail = defineComponent({
|
|
29
|
+
name: "LogAlertEmail",
|
|
30
|
+
props: {
|
|
31
|
+
/** The app whose logs matched; `all apps` for a query across apps. */
|
|
32
|
+
app: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
/** The saved query's name. */
|
|
37
|
+
name: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: true
|
|
40
|
+
},
|
|
41
|
+
/** Its LogsQL. */
|
|
42
|
+
query: {
|
|
43
|
+
type: String,
|
|
44
|
+
required: true
|
|
45
|
+
},
|
|
46
|
+
count: {
|
|
47
|
+
type: Number,
|
|
48
|
+
required: true
|
|
49
|
+
},
|
|
50
|
+
threshold: {
|
|
51
|
+
type: Number,
|
|
52
|
+
required: true
|
|
53
|
+
},
|
|
54
|
+
/** How far back it counted, e.g. `5m`. */
|
|
55
|
+
window: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
/** How long it stays quiet now, e.g. `1h`. */
|
|
60
|
+
cooldown: {
|
|
61
|
+
type: String,
|
|
62
|
+
required: true
|
|
63
|
+
},
|
|
64
|
+
/** The logs, filtered to the query. */
|
|
65
|
+
url: {
|
|
66
|
+
type: String,
|
|
67
|
+
required: true
|
|
68
|
+
},
|
|
69
|
+
/** First lines of the newest matching entries. */
|
|
70
|
+
samples: {
|
|
71
|
+
type: Array,
|
|
72
|
+
default: () => []
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
setup(props) {
|
|
76
|
+
return () => h(WervtEmail, {
|
|
77
|
+
subject: `${props.app}: ${props.name} (${props.count} in ${props.window})`,
|
|
78
|
+
app: props.app,
|
|
79
|
+
preview: `${props.count} log ${props.count === 1 ? "entry" : "entries"} matched in the last ${spelledOut(props.window)}`,
|
|
80
|
+
footer: "Sent by wervt",
|
|
81
|
+
reason: `You received this email because you own or help run ${props.app} and it has an alert on this query.`
|
|
82
|
+
}, () => [
|
|
83
|
+
h(EmailHeading, { eyebrow: "Log alert" }, () => props.name),
|
|
84
|
+
h(EmailText, () => [
|
|
85
|
+
`${props.count} log ${props.count === 1 ? "entry" : "entries"} of ${props.app} matched `,
|
|
86
|
+
code(props.query),
|
|
87
|
+
` in the last ${spelledOut(props.window)}. The alert fires at ${props.threshold}.`
|
|
88
|
+
]),
|
|
89
|
+
...props.samples.length ? [h(EmailMuted, () => "Newest matches:"), h("pre", {
|
|
90
|
+
class: "wv-code",
|
|
91
|
+
style: {
|
|
92
|
+
margin: "0 0 24px",
|
|
93
|
+
padding: "12px 14px",
|
|
94
|
+
fontFamily: emailTheme.mono,
|
|
95
|
+
fontSize: "12px",
|
|
96
|
+
lineHeight: "18px",
|
|
97
|
+
whiteSpace: "pre-wrap",
|
|
98
|
+
wordBreak: "break-word",
|
|
99
|
+
color: emailTheme.highlighted,
|
|
100
|
+
background: emailTheme.codeBackground,
|
|
101
|
+
border: `1px solid ${emailTheme.accentBorder}`,
|
|
102
|
+
borderRadius: "8px"
|
|
103
|
+
}
|
|
104
|
+
}, props.samples.join("\n"))] : [],
|
|
105
|
+
h(EmailButton, {
|
|
106
|
+
href: props.url,
|
|
107
|
+
color: "neutral"
|
|
108
|
+
}, () => "Open the logs"),
|
|
109
|
+
h(EmailDivider),
|
|
110
|
+
h(EmailMuted, () => `No more mails about this for ${spelledOut(props.cooldown)}, even if entries keep matching. Change or turn off the alert where the query is saved.`)
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
export { LogAlertEmail };
|
|
@@ -27,6 +27,19 @@ export declare const emailTheme: {
|
|
|
27
27
|
site: string;
|
|
28
28
|
docs: string;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Another product's look instead of wervt's, for the login's mails to people signing in to an
|
|
32
|
+
* app with its own branding (`wervt.brand`). wervt is then only named in the footer.
|
|
33
|
+
*/
|
|
34
|
+
export interface EmailBrand {
|
|
35
|
+
name: string;
|
|
36
|
+
/** An absolute URL of a square PNG or JPEG; mail clients don't show SVG. */
|
|
37
|
+
icon?: string;
|
|
38
|
+
/** Hex color for the line under the header. */
|
|
39
|
+
color?: string;
|
|
40
|
+
/** `false` leaves "Secured by wervt" out of the footer. */
|
|
41
|
+
securedByWervt?: boolean;
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* The whole email: document, preheader, header with the logo, body and footer. Put it at the
|
|
32
45
|
* root of every template.
|
|
@@ -61,6 +74,11 @@ export declare const WervtEmail: import("vue").DefineComponent<import("vue").Ext
|
|
|
61
74
|
type: StringConstructor;
|
|
62
75
|
default: string;
|
|
63
76
|
};
|
|
77
|
+
/** Shows another product's name and icon instead of wervt's (see `EmailBrand`). */
|
|
78
|
+
brand: {
|
|
79
|
+
type: PropType<EmailBrand | null>;
|
|
80
|
+
default: null;
|
|
81
|
+
};
|
|
64
82
|
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
65
83
|
[key: string]: any;
|
|
66
84
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -89,11 +107,17 @@ export declare const WervtEmail: import("vue").DefineComponent<import("vue").Ext
|
|
|
89
107
|
type: StringConstructor;
|
|
90
108
|
default: string;
|
|
91
109
|
};
|
|
110
|
+
/** Shows another product's name and icon instead of wervt's (see `EmailBrand`). */
|
|
111
|
+
brand: {
|
|
112
|
+
type: PropType<EmailBrand | null>;
|
|
113
|
+
default: null;
|
|
114
|
+
};
|
|
92
115
|
}>> & Readonly<{}>, {
|
|
93
116
|
app: string;
|
|
94
117
|
preview: string;
|
|
95
118
|
footer: string;
|
|
96
119
|
reason: string;
|
|
120
|
+
brand: EmailBrand | null;
|
|
97
121
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
98
122
|
export declare const EmailHeading: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
99
123
|
/** A short label above the heading, in the landing page's mono eyebrow style. */
|
|
@@ -29,6 +29,8 @@ const emailTheme = {
|
|
|
29
29
|
docs: "https://wervt.app/getting-started/introduction"
|
|
30
30
|
};
|
|
31
31
|
const t = emailTheme;
|
|
32
|
+
/** Icons mail clients can show: Gmail and Outlook drop SVG, Outlook has no WebP. */
|
|
33
|
+
const mailIcon = (brand) => brand.icon && /^https:\/\/\S+\.(png|jpe?g|gif)(\?\S*)?$/i.test(brand.icon) ? brand.icon : void 0;
|
|
32
34
|
const darkStyles = `
|
|
33
35
|
:root { color-scheme: light dark; supported-color-schemes: light dark; }
|
|
34
36
|
@media (prefers-color-scheme: dark) {
|
|
@@ -89,6 +91,11 @@ const WervtEmail = defineComponent({
|
|
|
89
91
|
reason: {
|
|
90
92
|
type: String,
|
|
91
93
|
default: ""
|
|
94
|
+
},
|
|
95
|
+
/** Shows another product's name and icon instead of wervt's (see `EmailBrand`). */
|
|
96
|
+
brand: {
|
|
97
|
+
type: Object,
|
|
98
|
+
default: null
|
|
92
99
|
}
|
|
93
100
|
},
|
|
94
101
|
setup(props, { slots }) {
|
|
@@ -146,10 +153,10 @@ const WervtEmail = defineComponent({
|
|
|
146
153
|
stub(),
|
|
147
154
|
rule(true, none, {
|
|
148
155
|
color: t.band,
|
|
149
|
-
image: `url('${t.assets}/header.png') right top / 600px 100px no-repeat`
|
|
156
|
+
image: props.brand ? void 0 : `url('${t.assets}/header.png') right top / 600px 100px no-repeat`
|
|
150
157
|
}),
|
|
151
|
-
framed(header(props.app), { padding: 0 }),
|
|
152
|
-
framed(accentLine(), { padding: 0 }),
|
|
158
|
+
framed(props.brand ? brandHeader(props.brand) : header(props.app), { padding: 0 }),
|
|
159
|
+
framed(accentLine(props.brand?.color), { padding: 0 }),
|
|
153
160
|
framed(slots.default?.(), {
|
|
154
161
|
class: "wv-card wv-pad",
|
|
155
162
|
background: t.card,
|
|
@@ -233,21 +240,66 @@ function header(app) {
|
|
|
233
240
|
whiteSpace: "nowrap"
|
|
234
241
|
} }, [h("span", { style: { color: "#5885ff" } }, "●"), `\u00A0\u00A0${app}`])])]))])]);
|
|
235
242
|
}
|
|
236
|
-
/** The
|
|
237
|
-
function
|
|
243
|
+
/** The band on top for a branded mail: the product's icon and name, no wervt dot grid. */
|
|
244
|
+
function brandHeader(brand) {
|
|
245
|
+
const icon = mailIcon(brand);
|
|
246
|
+
const name = h("td", {
|
|
247
|
+
class: "wv-pad",
|
|
248
|
+
style: {
|
|
249
|
+
padding: icon ? "30px 40px 30px 12px" : "30px 40px",
|
|
250
|
+
verticalAlign: "middle",
|
|
251
|
+
fontFamily: t.font,
|
|
252
|
+
fontSize: "20px",
|
|
253
|
+
lineHeight: "32px",
|
|
254
|
+
fontWeight: 600,
|
|
255
|
+
letterSpacing: "-0.3px",
|
|
256
|
+
color: "#fafafa"
|
|
257
|
+
}
|
|
258
|
+
}, brand.name);
|
|
259
|
+
return table({
|
|
260
|
+
width: "100%",
|
|
261
|
+
bgcolor: t.band,
|
|
262
|
+
style: { background: t.band }
|
|
263
|
+
}, [h("tr", [icon ? h("td", {
|
|
264
|
+
class: "wv-pad wervt-skip-text",
|
|
265
|
+
width: 72,
|
|
266
|
+
style: {
|
|
267
|
+
padding: "30px 0 30px 40px",
|
|
268
|
+
verticalAlign: "middle",
|
|
269
|
+
width: "72px"
|
|
270
|
+
}
|
|
271
|
+
}, h("img", {
|
|
272
|
+
src: icon,
|
|
273
|
+
width: 32,
|
|
274
|
+
height: 32,
|
|
275
|
+
alt: "",
|
|
276
|
+
style: {
|
|
277
|
+
display: "block",
|
|
278
|
+
width: "32px",
|
|
279
|
+
height: "32px",
|
|
280
|
+
border: 0,
|
|
281
|
+
borderRadius: "7px"
|
|
282
|
+
}
|
|
283
|
+
})) : null, name])]);
|
|
284
|
+
}
|
|
285
|
+
/** The landing page's headline gradient, as a hairline under the header; a brand's own color. */
|
|
286
|
+
function accentLine(color) {
|
|
287
|
+
const solid = color && /^#[0-9a-f]{3,6}$/i.test(color) ? color : void 0;
|
|
238
288
|
return table({ width: "100%" }, [h("tr", [h("td", {
|
|
239
289
|
height: 2,
|
|
240
|
-
bgcolor: t.accent,
|
|
290
|
+
bgcolor: solid ?? t.accent,
|
|
241
291
|
style: {
|
|
242
292
|
height: "2px",
|
|
243
293
|
fontSize: 0,
|
|
244
294
|
lineHeight: 0,
|
|
245
|
-
background: `${t.accent} linear-gradient(90deg, #60a5fa, ${t.accent} 45%, #a78bfa)`
|
|
295
|
+
background: solid ?? `${t.accent} linear-gradient(90deg, #60a5fa, ${t.accent} 45%, #a78bfa)`
|
|
246
296
|
}
|
|
247
297
|
})])]);
|
|
248
298
|
}
|
|
249
299
|
function footer(props) {
|
|
250
|
-
const
|
|
300
|
+
const { brand } = props;
|
|
301
|
+
const sentBy = props.footer || (brand ? `Sent by ${brand.name}` : props.app === "wervt" ? "Sent by wervt" : `Sent by ${props.app} via wervt`);
|
|
302
|
+
const tile = brand ? mailIcon(brand) : `${t.assets}/mark-tile.png`;
|
|
251
303
|
const small = {
|
|
252
304
|
fontFamily: t.font,
|
|
253
305
|
fontSize: "12px",
|
|
@@ -257,7 +309,7 @@ function footer(props) {
|
|
|
257
309
|
color: t.muted,
|
|
258
310
|
textDecoration: "none"
|
|
259
311
|
};
|
|
260
|
-
return table({ width: "100%" }, [h("tr", [h("td", {
|
|
312
|
+
return table({ width: "100%" }, [h("tr", [tile ? h("td", {
|
|
261
313
|
width: 40,
|
|
262
314
|
style: {
|
|
263
315
|
width: "40px",
|
|
@@ -265,7 +317,7 @@ function footer(props) {
|
|
|
265
317
|
paddingTop: "2px"
|
|
266
318
|
}
|
|
267
319
|
}, h("img", {
|
|
268
|
-
src:
|
|
320
|
+
src: tile,
|
|
269
321
|
width: 28,
|
|
270
322
|
height: 28,
|
|
271
323
|
alt: "",
|
|
@@ -276,7 +328,7 @@ function footer(props) {
|
|
|
276
328
|
border: 0,
|
|
277
329
|
borderRadius: "7px"
|
|
278
330
|
}
|
|
279
|
-
})), h("td", { style: { verticalAlign: "top" } }, [
|
|
331
|
+
})) : null, h("td", { style: { verticalAlign: "top" } }, [
|
|
280
332
|
h("p", {
|
|
281
333
|
class: "wv-strong",
|
|
282
334
|
style: {
|
|
@@ -304,7 +356,11 @@ function footer(props) {
|
|
|
304
356
|
fontSize: "11px",
|
|
305
357
|
color: t.dimmed
|
|
306
358
|
}
|
|
307
|
-
}, [
|
|
359
|
+
}, brand ? brand.securedByWervt === false ? [] : [h("a", {
|
|
360
|
+
href: t.site,
|
|
361
|
+
class: "wv-muted",
|
|
362
|
+
style: link
|
|
363
|
+
}, "Secured by wervt")] : [
|
|
308
364
|
h("a", {
|
|
309
365
|
href: t.site,
|
|
310
366
|
class: "wv-muted",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LogAlertEmail } from "./alert.js";
|
|
2
|
+
import { EmailBrand, EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
3
|
import { InviteEmail } from "./invite.js";
|
|
3
4
|
import { EmailProps, RenderedEmail, renderEmail } from "./render.js";
|
|
4
|
-
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, type EmailProps, EmailText, InviteEmail, type RenderedEmail, WervtEmail, emailTheme, renderEmail };
|
|
5
|
+
export { type EmailBrand, EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, type EmailProps, EmailText, InviteEmail, LogAlertEmail, type RenderedEmail, WervtEmail, emailTheme, renderEmail };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { LogAlertEmail } from "./alert.js";
|
|
2
3
|
import { InviteEmail } from "./invite.js";
|
|
3
4
|
import { renderEmail } from "./render.js";
|
|
4
|
-
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, InviteEmail, WervtEmail, emailTheme, renderEmail };
|
|
5
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, InviteEmail, LogAlertEmail, WervtEmail, emailTheme, renderEmail };
|
|
@@ -9,8 +9,8 @@ import { McpTokenContext } from "./mcp-auth.js";
|
|
|
9
9
|
import { seedTable } from "./seed.js";
|
|
10
10
|
import { ShapeResponse } from "../shared/shape.js";
|
|
11
11
|
import { ShapeOptions, ShapeRow, proxyShape } from "./shape.js";
|
|
12
|
-
import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
12
|
+
import { AccountMergedPayload, Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
13
13
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
14
14
|
import { Tool, ToolRoute, defineTool, defineToolRoute } from "./tools.js";
|
|
15
15
|
import { requireWervtRole, useWervtUser } from "./user.js";
|
|
16
|
-
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 WervtRole, type WervtUser, type WorkloadIdentity, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, requireWervtRole, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, useWorkloadIdentity, vh };
|
|
16
|
+
export { type AccountMergedPayload, 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 WervtRole, type WervtUser, type WorkloadIdentity, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, requireWervtRole, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, useWorkloadIdentity, vh };
|
|
@@ -8,6 +8,11 @@ export interface SendMailOptions<T extends Component> {
|
|
|
8
8
|
props?: EmailProps<T>;
|
|
9
9
|
/** Where replies go. The sender is always `<app> <mail@wervt.app>`. */
|
|
10
10
|
replyTo?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The sender's display name instead of the app's. Only the wervt login may set it (for the app
|
|
13
|
+
* someone signs in to); for every other app the platform ignores it.
|
|
14
|
+
*/
|
|
15
|
+
fromName?: string;
|
|
11
16
|
}
|
|
12
17
|
/**
|
|
13
18
|
* Renders a template and sends it through wervt. Throws on failure, with the status the platform
|
|
@@ -11,8 +11,8 @@ type Registry = Record<string, {
|
|
|
11
11
|
}>;
|
|
12
12
|
/** `POST /_wervt/tasks/:name`: runs a task when the control plane asks. */
|
|
13
13
|
export declare function taskRunHandler(runTask: RunTask): import("nuxt/server").EventHandler<Promise<{
|
|
14
|
-
result?: undefined;
|
|
15
14
|
error: string;
|
|
15
|
+
result?: undefined;
|
|
16
16
|
} | {
|
|
17
17
|
error?: undefined;
|
|
18
18
|
result: {} | null;
|
|
@@ -4,6 +4,20 @@ export interface TaskEvent<P = Record<string, unknown>> {
|
|
|
4
4
|
payload: P;
|
|
5
5
|
context: Record<string, unknown>;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Payload of the `wervt:account-merged` task: one person had two wervt accounts and an admin
|
|
9
|
+
* merged `from` into `into`. `from` no longer exists; move its rows over to `into`.
|
|
10
|
+
*/
|
|
11
|
+
export interface AccountMergedPayload {
|
|
12
|
+
from: {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
};
|
|
16
|
+
into: {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
7
21
|
export interface TaskResult<R = unknown> {
|
|
8
22
|
result?: R;
|
|
9
23
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface ObjectDetails extends StoredObject {
|
|
|
43
43
|
export declare const MAX_KEY_BYTES = 1024;
|
|
44
44
|
export declare function createStorage(options: StorageOptions): {
|
|
45
45
|
ensureLayout: () => Promise<void>;
|
|
46
|
-
provision: (app: string,
|
|
46
|
+
provision: (app: string, appOrigins: string | string[], quota?: StorageQuota) => Promise<{
|
|
47
47
|
bucket: string;
|
|
48
48
|
}>;
|
|
49
49
|
setQuota: (app: string, quota: StorageQuota) => Promise<boolean>;
|
package/dist/storage.js
CHANGED
|
@@ -119,11 +119,11 @@ function createStorage(options) {
|
|
|
119
119
|
};
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* Creates the app's bucket and key if they're missing, lets the app's
|
|
122
|
+
* Creates the app's bucket and key if they're missing, lets the app's origins upload and
|
|
123
123
|
* download from browsers (CORS), and cleans up abandoned uploads. Safe to run on every deploy.
|
|
124
124
|
* With `quota`, sets the bucket's size limit too; without, leaves it as it is.
|
|
125
125
|
*/
|
|
126
|
-
async function provision(app,
|
|
126
|
+
async function provision(app, appOrigins, quota) {
|
|
127
127
|
const { creds, client, base } = await s3(app);
|
|
128
128
|
const bucket = await exists(`GetBucketInfo?globalAlias=${creds.bucket}`) ?? await admin("CreateBucket", { globalAlias: creds.bucket });
|
|
129
129
|
if (!await exists(`GetKeyInfo?id=${creds.accessKeyId}`)) await admin("ImportKey", {
|
|
@@ -142,8 +142,7 @@ function createStorage(options) {
|
|
|
142
142
|
});
|
|
143
143
|
if (quota) await applyQuota(bucket.id, quota);
|
|
144
144
|
const cors = `<CORSConfiguration><CORSRule>
|
|
145
|
-
|
|
146
|
-
<AllowedMethod>GET</AllowedMethod><AllowedMethod>HEAD</AllowedMethod>
|
|
145
|
+
${[appOrigins].flat().map((origin) => `<AllowedOrigin>${origin}</AllowedOrigin>\n`).join("")}<AllowedMethod>GET</AllowedMethod><AllowedMethod>HEAD</AllowedMethod>
|
|
147
146
|
<AllowedMethod>PUT</AllowedMethod><AllowedMethod>POST</AllowedMethod>
|
|
148
147
|
<AllowedHeader>*</AllowedHeader><ExposeHeader>ETag</ExposeHeader>
|
|
149
148
|
<MaxAgeSeconds>3600</MaxAgeSeconds>
|