@lenne.tech/nuxt-extensions 1.23.0 → 1.24.1

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/CLAUDE.md CHANGED
@@ -13,7 +13,7 @@ src/
13
13
  │ ├── lib/ # Library exports (auth-client, auth-state, ai)
14
14
  │ ├── middleware/ # Route middleware (system setup)
15
15
  │ ├── plugins/ # Nuxt plugins (auth interceptor, error translation, config check)
16
- │ ├── server/ # server-side tsconfig scope (no runtime routes; auth talks to the backend directly)
16
+ │ ├── server/ # Nitro code: the opt-in security.txt route (auth talks to the backend directly, no proxy)
17
17
  │ ├── testing/ # Playwright test helpers
18
18
  │ ├── types/ # TypeScript type definitions
19
19
  │ └── utils/ # Auto-imported utility functions
@@ -96,6 +96,10 @@ export default defineNuxtConfig({
96
96
  enabled: true, // keep text typed before hydration instead of losing it
97
97
  maxRestoreMs: 1500, // how long to keep repairing deferred subtrees
98
98
  },
99
+ securityTxt: {
100
+ enabled: false, // opt-in: /.well-known/security.txt (RFC 9116)
101
+ contact: 'mailto:security@lenne.tech',
102
+ },
99
103
  },
100
104
  });
101
105
  ```
@@ -404,6 +408,39 @@ fields at `app:beforeMount` and restores them at `app:mounted`, dispatching a sy
404
408
  takes over.
405
409
  - Opt out with `ltExtensions.preHydrationInput.enabled: false`.
406
410
 
411
+ ## security.txt (RFC 9116)
412
+
413
+ `ltExtensions.securityTxt.enabled: true` serves `/.well-known/security.txt`. Contact defaults to
414
+ `mailto:security@lenne.tech`. Builder: `runtime/server/lib/security-txt.ts` (pure, tested in
415
+ `test/security-txt.test.ts`). Route: `runtime/server/routes/security-txt.ts`.
416
+
417
+ - **Do not replace it with a static file in `public/`.** `Expires` is computed per request so it
418
+ never runs out between deployments. A static file expires silently, and scanners drop it.
419
+ - **Do not register it with a method.** `addServerHandler` without `method` answers HEAD too;
420
+ scanners probe with HEAD.
421
+ - **Values live in the server-only runtime config** (`runtimeConfig.ltExtensions.securityTxt`), so
422
+ `NUXT_LT_EXTENSIONS_SECURITY_TXT_*` overrides them per stage. `enabled` is build-time only.
423
+ - **`Canonical` from the request host only via the `hosts` allowlist.** The host is a request
424
+ header; anything not listed must never reach the file.
425
+ - **No list defaults in `defaultOptions`.** `defineNuxtModule` merges them with `defu`, which
426
+ concatenates arrays: a default `['de', 'en']` turned a project's `['en']` into `en, de, en`. Set
427
+ list defaults where the option is read. `test/security-txt.test.ts` runs the real merge.
428
+ - **A contact that is not a `mailto:`, `https://` or `tel:` URI counts as blank**, so a bare address
429
+ ends in a 404 rather than a file scanners reject. If a project reports a 404 with the option on,
430
+ check the contact first.
431
+ - For a customer project, ask whether reports go to lenne.Tech or the customer before enabling it.
432
+ - **No `tsconfig.json` under `src/runtime`.** Vendoring copies that tree one level deeper, so a
433
+ relative `extends` points at nothing, and `oxlint --type-aware` then stops every vendored project's
434
+ `check` with `Invalid tsconfig`. Nitro code is typechecked through the root `tsconfig.server.json`.
435
+ No gate here runs in vendor mode, so `test/vendored-core.test.ts` holds this.
436
+ - **nuxt-base-starter lints `app/core/**` with a subset of the plugins in `.oxlintrc.json`, never
437
+ more**, and `lint` here runs with `--deny-warnings`. A plugin only the starter applies reports its
438
+ warnings in every generated project after the release; the 1.24.0 smoke test found three. A plugin
439
+ the starter wants for `app/core/**` is added here first.
440
+ - **The `nuxt/schema` blocks in `runtime/types/module.ts` augment `PublicRuntimeConfig` and nothing
441
+ else.** Vendoring turns them into project source, where they cause TS2310, and the cli removes a
442
+ block only while it holds that one interface. Type server-only config where it is read.
443
+
407
444
  ## Development Rules
408
445
 
409
446
  1. **ALWAYS read source code** in `dist/runtime/` to understand available composables and components
package/README.md CHANGED
@@ -97,6 +97,7 @@ overrides:
97
97
  - **Offline Sync** - Read and write offline, reconcile with the nest-server sync module (opt-in)
98
98
  - **Transition Components** - Ready-to-use Vue transition wrappers
99
99
  - **i18n Support** - English and German translations (works without i18n too)
100
+ - **security.txt** - Opt-in RFC 9116 file that never expires between deployments
100
101
  - **Auto-imports** - All composables and components are auto-imported
101
102
  - **Pre-hydration input preservation** - text typed before hydration is kept, not erased
102
103
  - **Form label repair** - restores `<label for>` associations that hydration breaks
@@ -213,6 +214,12 @@ export default defineNuxtConfig({
213
214
  basePath: '/ai', // Must match the nest-server AI controller
214
215
  },
215
216
 
217
+ // security.txt at /.well-known/security.txt (see "security.txt" below)
218
+ securityTxt: {
219
+ enabled: false, // Opt-in
220
+ contact: 'mailto:security@lenne.tech',
221
+ },
222
+
216
223
  // Offline sync (opt-in, see "Offline Sync" below)
217
224
  sync: {
218
225
  enabled: false, // Enable useLtOfflineSync() and the sign-out cleanup
@@ -378,6 +385,61 @@ export default defineNuxtConfig({
378
385
  });
379
386
  ```
380
387
 
388
+ ## security.txt (RFC 9116)
389
+
390
+ A `security.txt` tells security researchers where to report a vulnerability. The module can serve
391
+ one at `/.well-known/security.txt`. It is **off by default**: only turn it on where somebody
392
+ actually reads the contact address. A published address that nobody answers is worse than none.
393
+
394
+ ```ts
395
+ export default defineNuxtConfig({
396
+ ltExtensions: {
397
+ securityTxt: {
398
+ enabled: true,
399
+ // All optional:
400
+ contact: 'mailto:security@lenne.tech', // default; string or array, mailto:/https:/tel:
401
+ hosts: ['example.com', 'www.example.com'], // hostnames served without a redirect
402
+ preferredLanguages: ['de', 'en'], // default
403
+ validDays: 180, // default; clamped to 365
404
+ policy: '/security-policy', // a path is resolved against the site URL
405
+ },
406
+ },
407
+ });
408
+ ```
409
+
410
+ Response for `https://example.com`:
411
+
412
+ ```text
413
+ Contact: mailto:security@lenne.tech
414
+ Expires: 2027-04-26T00:00:00.000Z
415
+ Preferred-Languages: de, en
416
+ Canonical: https://example.com/.well-known/security.txt
417
+ ```
418
+
419
+ **For a customer project, decide where reports go.** The default sends them to lenne.Tech. If the
420
+ customer wants reports themselves, set their address.
421
+
422
+ How it behaves:
423
+
424
+ - **`Expires` never runs out.** The file is built per request: "now + `validDays`", cut to
425
+ midnight UTC. A file written once at deploy time would expire on its own, and scanners such as
426
+ the BSI's drop an expired file without telling anyone.
427
+ - **`Canonical`** names `runtimeConfig.public.siteUrl` (`NUXT_PUBLIC_SITE_URL`; the module declares
428
+ the key while the option is on). If the site answers under more than one hostname without a redirect (apex
429
+ and `www`), list them in `hosts`. A request for one of them adds a second `Canonical` line,
430
+ because RFC 9116 expects the URL a file was fetched from to be listed. The host comes from a
431
+ request header, so only hosts on that list ever reach the file.
432
+ - **GET and HEAD** both answer `200` with `text/plain; charset=utf-8` and `cache-control: no-store`.
433
+ - **Overridable per stage at runtime**, without a rebuild:
434
+ `NUXT_LT_EXTENSIONS_SECURITY_TXT_CONTACT`, `…_HOSTS` (`"example.com,www.example.com"`),
435
+ `…_PREFERRED_LANGUAGES`, `…_VALID_DAYS`, `…_POLICY`. Only `enabled` is fixed at build time.
436
+ - **A contact must be a URI.** `mailto:`, `https://` or `tel:`; a bare `security@example.com` counts
437
+ as no contact. Without a usable contact, including a blanked one, the route answers `404`, because
438
+ a file without `Contact` is invalid.
439
+ - **Never prerendered.** The route is excluded from prerendering, so `Expires` is not frozen at
440
+ build time. A fully static site (`nuxt generate`) has no server to answer it at all. The module
441
+ warns about that at build time.
442
+
381
443
  ## Usage
382
444
 
383
445
  ### Authentication
package/dist/module.d.mts CHANGED
@@ -3,7 +3,7 @@ import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
3
3
  export * from '../dist/runtime/types/index.js';
4
4
 
5
5
  declare const name = "@lenne.tech/nuxt-extensions";
6
- declare const version = "1.23.0";
6
+ declare const version = "1.24.1";
7
7
  declare const configKey = "ltExtensions";
8
8
  declare const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
9
9
  declare const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "configKey": "ltExtensions",
3
3
  "name": "@lenne.tech/nuxt-extensions",
4
- "version": "1.23.0",
4
+ "version": "1.24.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComponent, addPlugin, addRouteMiddleware } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComponent, addPlugin, addRouteMiddleware, addServerHandler } from '@nuxt/kit';
2
2
  import { pathToFileURL } from 'node:url';
3
+ import { SECURITY_TXT_DEFAULT_VALID_DAYS, SECURITY_TXT_DEFAULT_LANGUAGES, SECURITY_TXT_DEFAULT_CONTACT, SECURITY_TXT_PATH } from '../dist/runtime/server/lib/security-txt.js';
3
4
 
4
5
  const name = "@lenne.tech/nuxt-extensions";
5
- const version = "1.23.0";
6
+ const version = "1.24.1";
6
7
  const configKey = "ltExtensions";
7
8
  const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
8
9
  const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
@@ -46,6 +47,12 @@ const defaultOptions = {
46
47
  preHydrationInput: {
47
48
  enabled: true
48
49
  },
50
+ // Only the switch. `defineNuxtModule` merges these defaults with `defu`, which concatenates arrays,
51
+ // so a list here would be appended to the project's own. The other defaults are set where the
52
+ // option is read, in the `securityTxt` block of `setup`.
53
+ securityTxt: {
54
+ enabled: false
55
+ },
49
56
  // Opt-in: an app without offline sync gets no auto-import and no sign-out cleanup
50
57
  sync: {
51
58
  basePath: "/sync",
@@ -79,6 +86,7 @@ const module$1 = defineNuxtModule({
79
86
  formLabelAssociation: { ...defaultOptions.formLabelAssociation, ...options.formLabelAssociation },
80
87
  i18n: { ...defaultOptions.i18n, ...options.i18n },
81
88
  preHydrationInput: { ...defaultOptions.preHydrationInput, ...options.preHydrationInput },
89
+ securityTxt: { ...defaultOptions.securityTxt, ...options.securityTxt },
82
90
  sync: { ...defaultOptions.sync, ...options.sync },
83
91
  tus: { ...defaultOptions.tus, ...options.tus }
84
92
  };
@@ -256,6 +264,33 @@ const module$1 = defineNuxtModule({
256
264
  if (resolvedOptions.formLabelAssociation.enabled !== false) {
257
265
  addPlugin(resolve("./runtime/plugins/form-label-association.client"));
258
266
  }
267
+ if (resolvedOptions.securityTxt.enabled === true) {
268
+ const securityTxt = resolvedOptions.securityTxt;
269
+ rc.public.siteUrl ??= "";
270
+ nuxt.options.runtimeConfig.ltExtensions = {
271
+ ...nuxt.options.runtimeConfig.ltExtensions,
272
+ securityTxt: {
273
+ contact: securityTxt.contact ?? SECURITY_TXT_DEFAULT_CONTACT,
274
+ hosts: securityTxt.hosts ?? [],
275
+ policy: securityTxt.policy ?? "",
276
+ preferredLanguages: securityTxt.preferredLanguages ?? SECURITY_TXT_DEFAULT_LANGUAGES,
277
+ validDays: securityTxt.validDays ?? SECURITY_TXT_DEFAULT_VALID_DAYS
278
+ }
279
+ };
280
+ addServerHandler({ handler: resolve("./runtime/server/routes/security-txt"), route: SECURITY_TXT_PATH });
281
+ nuxt.options.routeRules = {
282
+ ...nuxt.options.routeRules,
283
+ [SECURITY_TXT_PATH]: { ...nuxt.options.routeRules?.[SECURITY_TXT_PATH], prerender: false }
284
+ };
285
+ nuxt.options.nitro ??= {};
286
+ nuxt.options.nitro.prerender ??= {};
287
+ nuxt.options.nitro.prerender.ignore = [...nuxt.options.nitro.prerender.ignore ?? [], SECURITY_TXT_PATH];
288
+ if (nuxt.options._generate || nuxt.options.nitro.static) {
289
+ console.warn(
290
+ `[${name}] securityTxt is enabled, but this is a static build (nuxt generate): there is no server to answer ${SECURITY_TXT_PATH}, so the file will be missing. Serve it with a server build, or place a static file yourself and keep its Expires current.`
291
+ );
292
+ }
293
+ }
259
294
  nuxt.options.build.transpile.push(resolve("./runtime"));
260
295
  nuxt.options.build.transpile.push("tus-js-client");
261
296
  if (await tryResolveModule("tus-js-client", [pathToFileURL(`${nuxt.options.rootDir}/`)])) {
@@ -74,15 +74,15 @@ function connectHub(key, hub) {
74
74
  hub.close = openLtSyncHints({
75
75
  models: models.split(","),
76
76
  onConnect: () => {
77
- for (const subscriber of [...hub.subscribers]) subscriber.onConnect();
77
+ for (const subscriber of Array.from(hub.subscribers)) subscriber.onConnect();
78
78
  },
79
79
  onHint: (model) => {
80
- for (const subscriber of [...hub.subscribers]) if (subscriber.model === model) subscriber.onHint();
80
+ for (const subscriber of Array.from(hub.subscribers)) if (subscriber.model === model) subscriber.onHint();
81
81
  },
82
82
  onStop: (reason) => {
83
83
  hub.close = void 0;
84
84
  hub.models = "";
85
- for (const subscriber of [...hub.subscribers]) subscriber.onStop?.(reason);
85
+ for (const subscriber of Array.from(hub.subscribers)) subscriber.onStop?.(reason);
86
86
  },
87
87
  tenantId: key || void 0
88
88
  });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * security.txt according to RFC 9116, built per request.
3
+ *
4
+ * Ported from the lenne.Tech website (DEV-3602), where it has been live since 2026-10-13.
5
+ * Pure functions only, so the route stays a thin adapter and everything that decides the
6
+ * file's content is unit-testable without a Nitro context.
7
+ */
8
+ export declare const SECURITY_TXT_PATH = "/.well-known/security.txt";
9
+ export declare const SECURITY_TXT_DEFAULT_CONTACT = "mailto:security@lenne.tech";
10
+ export declare const SECURITY_TXT_DEFAULT_LANGUAGES: string[];
11
+ export declare const SECURITY_TXT_DEFAULT_VALID_DAYS = 180;
12
+ export interface SecurityTxtOptions {
13
+ /** Contact URIs (`mailto:`, `https:` or `tel:`), in order of preference. */
14
+ contacts: string[];
15
+ /** Point in time the file is built for; `Expires` is derived from it. */
16
+ now: Date;
17
+ /** Origins the file is served from; each becomes one `Canonical` line. */
18
+ origins: string[];
19
+ /** URI of the vulnerability disclosure policy, or a path resolved against the first origin. */
20
+ policy?: string;
21
+ /** Language tags in order of preference. */
22
+ preferredLanguages?: string[];
23
+ /** Days between `now` and `Expires`. */
24
+ validDays?: number;
25
+ }
26
+ /**
27
+ * Normalises the configured contact(s). Accepts a string or an array, because a runtime override
28
+ * via `NUXT_LT_EXTENSIONS_SECURITY_TXT_CONTACT` arrives as a plain string.
29
+ *
30
+ * Whatever is not a usable URI is dropped. `security@example.com` is the obvious typo, and scanners
31
+ * treat the file it would produce as invalid; with no contact left, the route answers 404 instead.
32
+ */
33
+ export declare function normalizeSecurityTxtContacts(contact: unknown): string[];
34
+ /**
35
+ * Normalises a list option (languages, hosts). A runtime override via an environment variable
36
+ * arrives as a string, so `"de, en"` is accepted as well as `['de', 'en']`.
37
+ */
38
+ export declare function normalizeSecurityTxtList(value: unknown): string[];
39
+ /**
40
+ * Clamps `validDays` into the window RFC 9116 recommends. A non-finite or non-positive value
41
+ * falls back to the default instead of producing an `Expires` in the past.
42
+ */
43
+ export declare function normalizeSecurityTxtValidDays(validDays: unknown): number;
44
+ /**
45
+ * Turns a configured site URL into an origin (`https://example.com`), dropping path, query and
46
+ * trailing slash. Anything that is not an absolute http(s) URL yields `undefined`.
47
+ */
48
+ export declare function toSecurityTxtOrigin(url: string | undefined): string | undefined;
49
+ /**
50
+ * Turns the requested host into an origin, but only if it is one the site is known to be served
51
+ * from. The host comes from a request header, so anything else must not end up in the file.
52
+ */
53
+ export declare function allowedRequestOrigin(host: string | undefined, allowedHosts: string[]): string | undefined;
54
+ /**
55
+ * Builds a security.txt according to RFC 9116.
56
+ *
57
+ * `Expires` is relative to `now`, so a file built per request never goes stale, no matter how
58
+ * long ago the last deployment was. It is cut to midnight UTC, which makes the file valid for up
59
+ * to a day less than `validDays` and keeps it byte-identical for a whole day.
60
+ */
61
+ export declare function buildSecurityTxt(options: SecurityTxtOptions): string;
62
+ /** The server-only runtime config the module writes to `runtimeConfig.ltExtensions.securityTxt`. */
63
+ export interface SecurityTxtRuntimeConfig {
64
+ contact?: string | string[];
65
+ hosts?: string | string[];
66
+ policy?: string;
67
+ preferredLanguages?: string | string[];
68
+ validDays?: number | string;
69
+ }
70
+ /**
71
+ * Turns the runtime config and the request into the file's content, or `undefined` when no
72
+ * usable contact is left (the route then answers 404).
73
+ *
74
+ * `Canonical` names the configured site URL and, in addition, the requested host if it is one of
75
+ * the configured `hosts`. A site served under several hostnames without a redirect needs the
76
+ * second line: RFC 9116 expects the URI a file was fetched from among its Canonical lines.
77
+ */
78
+ export declare function resolveSecurityTxt(input: {
79
+ config: SecurityTxtRuntimeConfig | undefined;
80
+ now: Date;
81
+ requestHost: string | undefined;
82
+ siteUrl: string | undefined;
83
+ }): string | undefined;
@@ -0,0 +1,87 @@
1
+ export const SECURITY_TXT_PATH = "/.well-known/security.txt";
2
+ export const SECURITY_TXT_DEFAULT_CONTACT = "mailto:security@lenne.tech";
3
+ export const SECURITY_TXT_DEFAULT_LANGUAGES = ["de", "en"];
4
+ export const SECURITY_TXT_DEFAULT_VALID_DAYS = 180;
5
+ const SECURITY_TXT_MAX_VALID_DAYS = 365;
6
+ const DAY_MS = 24 * 60 * 60 * 1e3;
7
+ const CONTACT_URI = /^(?:mailto:|tel:|https:\/\/)\S/i;
8
+ function isSingleToken(value) {
9
+ return typeof value === "string" && value.length > 0 && !/\s/.test(value);
10
+ }
11
+ export function normalizeSecurityTxtContacts(contact) {
12
+ const values = Array.isArray(contact) ? contact : [contact];
13
+ return values.map((value) => typeof value === "string" ? value.trim() : value).filter(isSingleToken).filter((value) => CONTACT_URI.test(value));
14
+ }
15
+ export function normalizeSecurityTxtList(value) {
16
+ const values = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
17
+ return values.map((entry) => typeof entry === "string" ? entry.trim() : entry).filter(isSingleToken);
18
+ }
19
+ export function normalizeSecurityTxtValidDays(validDays) {
20
+ const days = Number(validDays);
21
+ if (!Number.isFinite(days) || days < 1) {
22
+ return SECURITY_TXT_DEFAULT_VALID_DAYS;
23
+ }
24
+ return Math.min(Math.floor(days), SECURITY_TXT_MAX_VALID_DAYS);
25
+ }
26
+ export function toSecurityTxtOrigin(url) {
27
+ if (!url) {
28
+ return void 0;
29
+ }
30
+ try {
31
+ const parsed = new URL(url);
32
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.origin : void 0;
33
+ } catch {
34
+ return void 0;
35
+ }
36
+ }
37
+ export function allowedRequestOrigin(host, allowedHosts) {
38
+ const name = host?.trim().toLowerCase();
39
+ const allowed = allowedHosts.map((entry) => entry.trim().toLowerCase());
40
+ return name && allowed.includes(name) ? `https://${name}` : void 0;
41
+ }
42
+ function resolveAgainstOrigin(value, origin) {
43
+ if (!isSingleToken(value)) {
44
+ return void 0;
45
+ }
46
+ if (!value.startsWith("/")) {
47
+ return value;
48
+ }
49
+ return origin ? `${origin}${value}` : void 0;
50
+ }
51
+ export function buildSecurityTxt(options) {
52
+ const { contacts, now, origins, policy, preferredLanguages = [] } = options;
53
+ const validDays = normalizeSecurityTxtValidDays(options.validDays ?? SECURITY_TXT_DEFAULT_VALID_DAYS);
54
+ const expires = new Date(Math.floor((now.getTime() + validDays * DAY_MS) / DAY_MS) * DAY_MS);
55
+ const canonicals = [...new Set(origins.map((origin) => origin.replace(/\/+$/, "")).filter(Boolean))];
56
+ const languages = preferredLanguages.map((language) => language.trim()).filter(isSingleToken);
57
+ const lines = [...contacts.filter(isSingleToken).map((contact) => `Contact: ${contact}`), `Expires: ${expires.toISOString()}`];
58
+ const policyUri = resolveAgainstOrigin(policy, canonicals[0]);
59
+ if (policyUri) {
60
+ lines.push(`Policy: ${policyUri}`);
61
+ }
62
+ if (languages.length) {
63
+ lines.push(`Preferred-Languages: ${languages.join(", ")}`);
64
+ }
65
+ for (const origin of canonicals) {
66
+ lines.push(`Canonical: ${origin}${SECURITY_TXT_PATH}`);
67
+ }
68
+ return `${lines.join("\n")}
69
+ `;
70
+ }
71
+ export function resolveSecurityTxt(input) {
72
+ const config = input.config ?? {};
73
+ const contacts = normalizeSecurityTxtContacts(config.contact);
74
+ if (!contacts.length) {
75
+ return void 0;
76
+ }
77
+ const siteOrigin = toSecurityTxtOrigin(input.siteUrl);
78
+ const requestOrigin = allowedRequestOrigin(input.requestHost, normalizeSecurityTxtList(config.hosts));
79
+ return buildSecurityTxt({
80
+ contacts,
81
+ now: input.now,
82
+ origins: [siteOrigin, requestOrigin].filter((origin) => !!origin),
83
+ policy: config.policy,
84
+ preferredLanguages: normalizeSecurityTxtList(config.preferredLanguages),
85
+ validDays: normalizeSecurityTxtValidDays(config.validDays)
86
+ });
87
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Serves `/.well-known/security.txt` (RFC 9116). Registered by the module only when
3
+ * `ltExtensions.securityTxt.enabled` is set.
4
+ *
5
+ * Registered without a method on purpose: scanners probe with HEAD as well as GET, and a
6
+ * GET-only handler would answer HEAD with the 404 catch-all.
7
+ */
8
+ declare const _default: any;
9
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { createError, defineEventHandler, getRequestHost, setResponseHeader, useRuntimeConfig } from "#imports";
2
+ import { resolveSecurityTxt } from "../lib/security-txt.js";
3
+ export default defineEventHandler((event) => {
4
+ const config = useRuntimeConfig(event);
5
+ const siteUrl = config.public?.siteUrl;
6
+ const ltExtensions = config.ltExtensions;
7
+ const body = resolveSecurityTxt({
8
+ config: ltExtensions?.securityTxt,
9
+ now: /* @__PURE__ */ new Date(),
10
+ requestHost: getRequestHost(event, { xForwardedHost: true }),
11
+ siteUrl: typeof siteUrl === "string" ? siteUrl : void 0
12
+ });
13
+ if (!body) {
14
+ throw createError({ statusCode: 404, statusMessage: "Not Found" });
15
+ }
16
+ setResponseHeader(event, "content-type", "text/plain; charset=utf-8");
17
+ setResponseHeader(event, "cache-control", "no-store");
18
+ return body;
19
+ });
@@ -1,6 +1,6 @@
1
1
  export type { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, LtUserInput, UseLtAuthReturn } from './auth.js';
2
2
  export type { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn } from './upload.js';
3
- export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtFormLabelAssociationOptions, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
3
+ export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtFormLabelAssociationOptions, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSecurityTxtOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
4
4
  export type { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from './error.js';
5
5
  export type { LtSyncConflict, LtSyncConflictResolution, LtSyncIdFormat, LtSyncItemStatus, LtSyncModuleOptions, LtSyncPushResult, LtSyncRecord, LtSyncResult, UseLtOfflineSyncOptions, UseLtOfflineSyncReturn, } from './sync.js';
6
6
  export type { LtAiAction, LtAiAvailableConnection, LtAiBudgetLimit, LtAiBudgetSummary, LtAiConnection, LtAiConnectionInput, LtAiConnectionPreference, LtAiEffectiveSlot, LtAiInteraction, LtAiMessage, LtAiMode, LtAiPlaceholder, LtAiPrompt, LtAiPromptHint, LtAiPromptHintInput, LtAiPromptInput, LtAiPromptRunInput, LtAiResponse, LtAiSettings, LtAiSettingsInput, LtAiSlot, LtAiStatus, LtAiSlotInput, LtAiStreamEvent, LtAiStreamHandlers, LtAiUsage, LtAiUsageInfo, LtAiUsageScope, UseLtAiAdminReturn, UseLtAiChatOptions, UseLtAiChatReturn, UseLtAiConnectionsReturn, UseLtAiPlaceholdersReturn, UseLtAiPromptsReturn, UseLtAiReturn, UseLtAiSettingsReturn, UseLtAiStatusReturn, UseLtAiUsageReturn, } from './ai.js';
@@ -148,6 +148,8 @@ export interface LtExtensionsModuleOptions {
148
148
  i18n?: LtI18nModuleOptions;
149
149
  /** Pre-hydration input preservation configuration */
150
150
  preHydrationInput?: LtPreHydrationInputOptions;
151
+ /** security.txt (RFC 9116) at `/.well-known/security.txt` */
152
+ securityTxt?: LtSecurityTxtOptions;
151
153
  /** Offline sync with the nest-server sync module (opt-in) */
152
154
  sync?: LtSyncModuleOptions;
153
155
  /** TUS upload module configuration */
@@ -271,6 +273,67 @@ export interface LtPreHydrationInputOptions {
271
273
  */
272
274
  maxRestoreMs?: number;
273
275
  }
276
+ /**
277
+ * security.txt according to RFC 9116, served at `/.well-known/security.txt`.
278
+ *
279
+ * The file tells security researchers where to report a vulnerability. Off by default: only turn
280
+ * it on where somebody actually reads the contact address — a published address that nobody
281
+ * answers is worse than none.
282
+ *
283
+ * The file is built per request. `Expires` is always "now + `validDays`", cut to midnight UTC, so
284
+ * it never runs out between deployments. `Canonical` names `runtimeConfig.public.siteUrl`
285
+ * (`NUXT_PUBLIC_SITE_URL`, the module declares the key) and, in addition, the requested host if it
286
+ * is listed in `hosts`.
287
+ *
288
+ * Everything except `enabled` can be overridden per stage at runtime, without a rebuild:
289
+ * `NUXT_LT_EXTENSIONS_SECURITY_TXT_CONTACT`, `…_HOSTS`, `…_POLICY`, `…_PREFERRED_LANGUAGES`,
290
+ * `…_VALID_DAYS`. Blanking the contact that way makes the route answer 404, because a file without
291
+ * `Contact` is invalid.
292
+ *
293
+ * Not available with `nuxt generate`: a static site has no server to compute the file.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * export default defineNuxtConfig({
298
+ * ltExtensions: {
299
+ * securityTxt: {
300
+ * enabled: true,
301
+ * contact: 'mailto:security@customer.example',
302
+ * hosts: ['customer.example', 'www.customer.example'],
303
+ * },
304
+ * },
305
+ * });
306
+ * ```
307
+ */
308
+ export interface LtSecurityTxtOptions {
309
+ /**
310
+ * Contact URI(s), in order of preference: `mailto:`, `https://` or `tel:`. Anything else, such
311
+ * as a bare address, counts as no contact, and the route answers 404.
312
+ * Defaults to `'mailto:security@lenne.tech'`. For a customer project, decide whether reports go
313
+ * to lenne.Tech or to the customer, and set it explicitly.
314
+ */
315
+ contact?: string | string[];
316
+ /** Serve the file. Defaults to `false`. */
317
+ enabled?: boolean;
318
+ /**
319
+ * Hostnames the site is served from without a redirect (e.g. apex and `www`). A request for one
320
+ * of them adds a second `Canonical` line for that host. The host comes from a request header,
321
+ * so nothing outside this list ever reaches the file. Defaults to `[]`.
322
+ */
323
+ hosts?: string[];
324
+ /**
325
+ * URI of the vulnerability disclosure policy. A path (`/security-policy`) is resolved against the
326
+ * site URL and left out when no site URL is configured.
327
+ */
328
+ policy?: string;
329
+ /** Language tags reports may be written in, in order of preference. Defaults to `['de', 'en']`. */
330
+ preferredLanguages?: string[];
331
+ /**
332
+ * Days between the request and `Expires`. Defaults to `180`; clamped to `365` because RFC 9116
333
+ * recommends less than a year, and a value below `1` falls back to the default.
334
+ */
335
+ validDays?: number;
336
+ }
274
337
  /**
275
338
  * Public runtime config added by this module
276
339
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nuxt-extensions",
3
- "version": "1.23.0",
3
+ "version": "1.24.1",
4
4
  "description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,8 +60,8 @@
60
60
  "dev:build": "nuxt build playground",
61
61
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
62
62
  "build": "nuxt-module-build build",
63
- "lint": "oxlint --type-aware src/ scripts/",
64
- "lint:fix": "oxlint --type-aware --fix src/ scripts/",
63
+ "lint": "oxlint --type-aware --deny-warnings src/ scripts/",
64
+ "lint:fix": "oxlint --type-aware --deny-warnings --fix src/ scripts/",
65
65
  "format": "oxfmt --write src/",
66
66
  "format:check": "oxfmt --check src/",
67
67
  "version:sync": "node scripts/sync-module-version.mjs",
@@ -69,7 +69,7 @@
69
69
  "test": "vitest run",
70
70
  "test:watch": "vitest watch",
71
71
  "test:coverage": "vitest run --coverage",
72
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
72
+ "test:types": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.server.json && cd playground && vue-tsc --noEmit",
73
73
  "release": "pnpm format:check && pnpm run dev:prepare && pnpm lint && pnpm run version:check && pnpm test:types && pnpm test && pnpm run check:consumer && pnpm prepack",
74
74
  "check:consumer": "node scripts/check-consumer-build.mjs",
75
75
  "check:manifest": "node scripts/check-package-manifest.mjs",
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "../../../.nuxt/tsconfig.server.json"
3
- }