@karibsen/eponyme 0.5.0 → 0.7.0

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 (68) hide show
  1. package/README.md +51 -39
  2. package/dist/eponyme.mjs +2 -3
  3. package/dist/module.d.mts +94 -13
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +23 -6
  6. package/dist/runtime/assets/icon.ico +0 -0
  7. package/dist/runtime/assets/logo.png +0 -0
  8. package/dist/runtime/assets.d.ts +9 -0
  9. package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
  10. package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
  11. package/dist/runtime/components/editor/EponymePreviewPanel.vue +4 -3
  12. package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
  13. package/dist/runtime/components/fields/FieldRenderer.vue +11 -0
  14. package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
  15. package/dist/runtime/components/fields/PhoneField.vue +61 -0
  16. package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
  17. package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
  18. package/dist/runtime/composables/useEponyme.d.ts +22 -14
  19. package/dist/runtime/composables/useEponyme.js +5 -3
  20. package/dist/runtime/composables/useEponymeCollection.js +6 -4
  21. package/dist/runtime/composables/useEponymeFavicon.d.ts +1 -0
  22. package/dist/runtime/composables/useEponymeFavicon.js +14 -0
  23. package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
  24. package/dist/runtime/composables/useEponymeForm.js +1 -8
  25. package/dist/runtime/fields/index.d.ts +2 -0
  26. package/dist/runtime/fields/index.js +2 -1
  27. package/dist/runtime/fields/phone.d.ts +2 -0
  28. package/dist/runtime/fields/phone.js +3 -0
  29. package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
  30. package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
  31. package/dist/runtime/pages/EponymeChangePasswordPage.vue +2 -0
  32. package/dist/runtime/pages/EponymeDetailPage.vue +2 -0
  33. package/dist/runtime/pages/EponymeIndexPage.vue +2 -0
  34. package/dist/runtime/pages/EponymeLoginPage.vue +9 -4
  35. package/dist/runtime/pages/EponymeUsersPage.vue +2 -0
  36. package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
  37. package/dist/runtime/server/middleware/eponyme-no-store.js +4 -1
  38. package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
  39. package/dist/runtime/server/services/eponyme-form-store.js +4 -3
  40. package/dist/runtime/server/services/eponyme-store.d.ts +11 -0
  41. package/dist/runtime/server/services/eponyme-store.js +36 -6
  42. package/dist/runtime/server/utils/eponyme-cache.d.ts +2 -14
  43. package/dist/runtime/server/utils/eponyme-cache.js +1 -6
  44. package/dist/runtime/types/eponyme-config.d.ts +0 -7
  45. package/dist/runtime/types/eponyme.d.ts +2 -5
  46. package/dist/runtime/types/field.d.ts +49 -4
  47. package/dist/runtime/types/index.d.ts +0 -1
  48. package/dist/runtime/types/index.js +0 -1
  49. package/dist/runtime/utils/cache-tags.d.ts +46 -0
  50. package/dist/runtime/utils/cache-tags.js +28 -0
  51. package/dist/runtime/utils/hydration-cache.d.ts +10 -0
  52. package/dist/runtime/utils/hydration-cache.js +3 -0
  53. package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
  54. package/dist/runtime/utils/middle-ellipsis.js +8 -0
  55. package/dist/runtime/utils/normalize-eponyme-phones.d.ts +9 -0
  56. package/dist/runtime/utils/normalize-eponyme-phones.js +38 -0
  57. package/dist/runtime/utils/normalize-phone.d.ts +26 -0
  58. package/dist/runtime/utils/normalize-phone.js +17 -0
  59. package/dist/runtime/utils/preview.d.ts +10 -0
  60. package/dist/runtime/utils/preview.js +7 -2
  61. package/dist/runtime/utils/validate-eponyme-data.js +29 -3
  62. package/package.json +3 -2
  63. package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
  64. package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
  65. package/dist/runtime/types/captcha.d.ts +0 -16
  66. package/dist/runtime/types/captcha.js +0 -0
  67. package/dist/runtime/utils/no-captcha.d.ts +0 -7
  68. package/dist/runtime/utils/no-captcha.js +0 -5
@@ -1,5 +1,4 @@
1
1
  export * from "./auth.js";
2
2
  export * from "./eponyme.js";
3
3
  export * from "./field.js";
4
- export * from "./captcha.js";
5
4
  export * from "./variables.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Single source of truth for Eponyme cache tags, shared by the API responses that carry
3
+ * them, the module that tags the host's page routes at build time, and any purge the host
4
+ * runs on publication. Kept free of server imports so `module.ts` can use it too: a page
5
+ * and its API response must be tagged by the same code, or a purge would drop one and
6
+ * leave the other.
7
+ *
8
+ * A CDN cannot usually be purged by URL — Vercel and Cloudflare both purge by tag — and one
9
+ * entry appears in more than one cached response: its own, its collection's listing, and the
10
+ * page that renders it. Tagging all of them is what lets publishing an article drop the
11
+ * article, the index that lists it, and the HTML a visitor would otherwise keep receiving.
12
+ *
13
+ * A comma is the delimiter every CDN uses for this header, so it is stripped rather than
14
+ * escaped: a tag containing one would silently become two.
15
+ */
16
+ export declare function getEponymeCacheTags(name: string, collection?: {
17
+ name: string;
18
+ } | string): string[];
19
+ /** The shape of `nuxt.options.routeRules` this needs, declared so this file stays build-safe. */
20
+ type TaggableRouteRules = Record<string, {
21
+ headers?: Record<string, string>;
22
+ }>;
23
+ /**
24
+ * Tags the host's public routes with the same tags their API responses carry.
25
+ *
26
+ * A published entry ends up in two caches: the JSON the browser reads, and — as soon as the
27
+ * host puts a route under `swr`, `isr` or `prerender` — the HTML itself. Only the first was
28
+ * tagged, so a purge on publication dropped the JSON and left visitors on the old page.
29
+ * `previewPaths` already declares which route renders which entry, which is exactly the
30
+ * mapping needed to tag the other one, once, at build time.
31
+ *
32
+ * A collection pattern cannot name a slug in a route rule, so its pages carry the
33
+ * collection-level tag: publishing one article purges every article page and the index that
34
+ * lists them. Broader than strictly needed, and never wrong — tagging per entry would mean
35
+ * tracking every entry read during a render, and nitro's request context is not shared
36
+ * across the internal fetch that reads them.
37
+ *
38
+ * A header the host wrote wins: this only fills in what nobody set. Returns what it did, so
39
+ * the module can say it out loud: writing into someone else's `routeRules` is invisible to
40
+ * whoever reads their own config, and an effect nobody can see is one nobody can debug.
41
+ */
42
+ export declare function tagPreviewPathRoutes(previewPaths: Record<string, string>, routeRules: TaggableRouteRules): {
43
+ route: string;
44
+ tag: string;
45
+ }[];
46
+ export {};
@@ -0,0 +1,28 @@
1
+ export function getEponymeCacheTags(name, collection) {
2
+ const collectionName = typeof collection === "string" ? collection : collection?.name;
3
+ const tags = ["eponyme", `eponyme:${name}`];
4
+ if (collectionName) tags.push(`eponyme:${collectionName}`);
5
+ return [...new Set(tags)].map((tag) => tag.replace(/,/g, "").slice(0, 256));
6
+ }
7
+ export function tagPreviewPathRoutes(previewPaths, routeRules) {
8
+ const tagged = [];
9
+ for (const [name, path] of Object.entries(previewPaths)) {
10
+ if (!path.startsWith("/")) continue;
11
+ const isCollection = path.includes(":slug");
12
+ const route = isCollection ? path.replace(/:slug\b.*$/, "**") : path;
13
+ const tags = getEponymeCacheTags(name, isCollection ? name : void 0);
14
+ const value = tags.join(",");
15
+ const existing = routeRules[route] ?? {};
16
+ routeRules[route] = {
17
+ ...existing,
18
+ headers: {
19
+ // Vercel reads its own header; `Cache-Tag` is what Cloudflare and Fastly-style CDNs read.
20
+ "Vercel-Cache-Tag": value,
21
+ "Cache-Tag": value,
22
+ ...existing.headers
23
+ }
24
+ };
25
+ tagged.push({ route, tag: tags[tags.length - 1] });
26
+ }
27
+ return tagged;
28
+ }
@@ -9,3 +9,13 @@ import type { NuxtApp } from '#app';
9
9
  * first paint faithful and still refetches on every later navigation.
10
10
  */
11
11
  export declare function cacheDuringHydrationOnly<T>(key: string, nuxtApp: NuxtApp): T | undefined;
12
+ /**
13
+ * `getCachedData` for one read, or `undefined` to keep Nuxt's own policy.
14
+ *
15
+ * Published content is the same for every visitor and already answers with a public
16
+ * `Cache-Control`, so Nuxt's default is exactly right: the SSR payload while hydrating,
17
+ * then `nuxtApp.static.data`, which prerendered and prefetched payloads fill. A client
18
+ * navigation to a prerendered route then costs no request at all. Unreleased material
19
+ * keeps refetching after hydration.
20
+ */
21
+ export declare function cacheForPublicRead(isPublic: boolean): typeof cacheDuringHydrationOnly | undefined;
@@ -1,3 +1,6 @@
1
1
  export function cacheDuringHydrationOnly(key, nuxtApp) {
2
2
  return nuxtApp.isHydrating ? nuxtApp.payload.data[key] : void 0;
3
3
  }
4
+ export function cacheForPublicRead(isPublic) {
5
+ return isPublic ? void 0 : cacheDuringHydrationOnly;
6
+ }
@@ -0,0 +1 @@
1
+ export declare function middleEllipsis(value: string, maxLength?: number): string;
@@ -0,0 +1,8 @@
1
+ export function middleEllipsis(value, maxLength = 28) {
2
+ const characters = [...value];
3
+ if (characters.length <= maxLength) return value;
4
+ const available = maxLength - 1;
5
+ const startLength = Math.ceil(available / 2);
6
+ const endLength = Math.floor(available / 2);
7
+ return `${characters.slice(0, startLength).join("")}\u2026${characters.slice(-endLength).join("")}`;
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { EponymeSchema } from '../types/index.js';
2
+ /**
3
+ * Rewrites every phone value in a payload to E.164, wherever it sits in the schema.
4
+ *
5
+ * Applied on the server before a write, which is what makes the stored format a guarantee
6
+ * rather than a convention: the API accepts any JSON, so a client normalising on its own could
7
+ * always be bypassed. Values that cannot be parsed are left as typed for validation to reject.
8
+ */
9
+ export declare function normalizeEponymePhones(schema: EponymeSchema, data: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,38 @@
1
+ import { isArrayItemFieldDefinition } from "./get-field-default-value.js";
2
+ import { toEponymePhoneValue } from "./normalize-phone.js";
3
+ export function normalizeEponymePhones(schema, data) {
4
+ const result = { ...data };
5
+ for (const [name, definition] of Object.entries(schema)) {
6
+ if (!(name in result)) continue;
7
+ result[name] = normalizeField(definition, result[name]);
8
+ }
9
+ return result;
10
+ }
11
+ function normalizeField(definition, value) {
12
+ if (definition.type === "phone") return toEponymePhoneValue(value, definition.options);
13
+ if (definition.type === "section")
14
+ return normalizeNested(definition.options.fields, value);
15
+ if (definition.type === "tabs") {
16
+ if (!isRecord(value)) return value;
17
+ const tabs = { ...value };
18
+ for (const [tabName, tab] of Object.entries(definition.options.tabs))
19
+ if (tabName in tabs) tabs[tabName] = normalizeNested(tab.fields, tabs[tabName]);
20
+ return tabs;
21
+ }
22
+ if (definition.type === "array") {
23
+ if (!Array.isArray(value)) return value;
24
+ return value.map((item) => normalizeArrayItem(definition.options.of, item));
25
+ }
26
+ return value;
27
+ }
28
+ function normalizeArrayItem(of, item) {
29
+ if (isArrayItemFieldDefinition(of)) return normalizeField(of, item);
30
+ return normalizeNested(of, item);
31
+ }
32
+ function normalizeNested(schema, value) {
33
+ if (!isRecord(value)) return value;
34
+ return normalizeEponymePhones(schema, value);
35
+ }
36
+ function isRecord(value) {
37
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
38
+ }
@@ -0,0 +1,26 @@
1
+ import type { PhoneFieldOptions } from '../types/field.js';
2
+ export interface NormalizedPhone {
3
+ /** The number in E.164, present only when it parsed and validated. */
4
+ e164?: string;
5
+ /** Country the number resolved to, present as soon as it parsed. */
6
+ country?: string;
7
+ valid: boolean;
8
+ /** Set when the number is valid but its country is not in `countries`. */
9
+ countryNotAllowed?: boolean;
10
+ }
11
+ /**
12
+ * Single source of truth for what a phone field accepts, shared by the validation the server
13
+ * enforces, the editor field, and the write path that stores the value.
14
+ *
15
+ * The `min` metadata bundle is deliberate: it is a fraction of the size of the full one and
16
+ * parses and formats every country. The trade is precision — it accepts a few numbers the full
17
+ * metadata would reject as impossible for their region.
18
+ *
19
+ * @see https://gitlab.com/catamphetamine/libphonenumber-js#min-vs-max-vs-mobile-vs-core
20
+ */
21
+ export declare function normalizeEponymePhone(value: unknown, options?: PhoneFieldOptions): NormalizedPhone;
22
+ /**
23
+ * The value to store: E.164 whenever the number is understood, and the input untouched when it
24
+ * is not, so validation can report it and the editor can still see what was typed.
25
+ */
26
+ export declare function toEponymePhoneValue(value: unknown, options?: PhoneFieldOptions): unknown;
@@ -0,0 +1,17 @@
1
+ import { parsePhoneNumberFromString } from "libphonenumber-js/min";
2
+ export function normalizeEponymePhone(value, options = {}) {
3
+ const raw = typeof value === "string" ? value.trim() : "";
4
+ if (!raw) return { valid: true };
5
+ const international = raw.startsWith("+");
6
+ if (options.detectCountry === false && !international) return { valid: false };
7
+ const parsed = parsePhoneNumberFromString(raw, international ? void 0 : options.defaultCountry);
8
+ if (!parsed || !parsed.isValid()) return { valid: false, country: parsed?.country };
9
+ const allowed = options.countries?.length ? options.countries : void 0;
10
+ if (allowed && (!parsed.country || !allowed.includes(parsed.country)))
11
+ return { valid: false, country: parsed.country, countryNotAllowed: true };
12
+ return { valid: true, e164: parsed.number, country: parsed.country };
13
+ }
14
+ export function toEponymePhoneValue(value, options = {}) {
15
+ if (typeof value !== "string") return value;
16
+ return normalizeEponymePhone(value, options).e164 ?? value;
17
+ }
@@ -20,6 +20,16 @@ export declare function splitCollectionEntry(collectionNames: string[], name: st
20
20
  * returns `previewPaths[collection]` with `:slug` filled in.
21
21
  */
22
22
  export declare function resolvePreviewPath(previewPaths: Record<string, string>, collectionNames: string[], name: string): string | undefined;
23
+ /**
24
+ * The query parameters that turn a public route into a preview.
25
+ *
26
+ * Named here because three places have to agree on them: the panel that builds the URL,
27
+ * the composables that read it, and the middleware that keeps such a response out of
28
+ * every cache. A typo in any one of them fails silently, in the direction of leaking.
29
+ */
30
+ export declare const EPONYME_PREVIEW_QUERY = "__eponyme_preview";
31
+ export declare const EPONYME_PREVIEW_VERSION_QUERY = "__eponyme_preview_version";
32
+ export declare const EPONYME_PREVIEW_TOKEN_QUERY = "__eponyme_preview_token";
23
33
  /** Reads `?__eponyme_preview_version=` — `draft` by default, or a numeric history id. */
24
34
  export declare function readPreviewVersion(raw: unknown): PreviewVersion;
25
35
  /** Reads the preview query params, unwrapping the repeated-param array form. */
@@ -18,13 +18,18 @@ export function resolvePreviewPath(previewPaths, collectionNames, name) {
18
18
  if (!pattern || !pattern.includes(":slug")) return void 0;
19
19
  return applyPreviewSlug(pattern, entry.slug);
20
20
  }
21
+ export const EPONYME_PREVIEW_QUERY = "__eponyme_preview";
22
+ export const EPONYME_PREVIEW_VERSION_QUERY = "__eponyme_preview_version";
23
+ export const EPONYME_PREVIEW_TOKEN_QUERY = "__eponyme_preview_token";
21
24
  export function readPreviewVersion(raw) {
22
25
  if (raw === "published") return "published";
23
26
  const id = Number(raw);
24
27
  return Number.isSafeInteger(id) && id > 0 ? id : "draft";
25
28
  }
26
29
  export function readPreviewQuery(query) {
27
- const entry = Array.isArray(query.__eponyme_preview) ? query.__eponyme_preview[0] : query.__eponyme_preview;
28
- const version = Array.isArray(query.__eponyme_preview_version) ? query.__eponyme_preview_version[0] : query.__eponyme_preview_version;
30
+ const rawEntry = query[EPONYME_PREVIEW_QUERY];
31
+ const rawVersion = query[EPONYME_PREVIEW_VERSION_QUERY];
32
+ const entry = Array.isArray(rawEntry) ? rawEntry[0] : rawEntry;
33
+ const version = Array.isArray(rawVersion) ? rawVersion[0] : rawVersion;
29
34
  return { entry: entry === void 0 || entry === null ? void 0 : String(entry), version };
30
35
  }
@@ -1,5 +1,6 @@
1
1
  import { isArrayItemFieldDefinition } from "./get-field-default-value.js";
2
2
  import { isFieldVisible } from "./is-field-visible.js";
3
+ import { normalizeEponymePhone } from "./normalize-phone.js";
3
4
  const urlPattern = /^https?:\/\//i;
4
5
  const datePattern = /^\d{4}-\d{2}-\d{2}$/;
5
6
  const colorPattern = /^#(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i;
@@ -134,7 +135,10 @@ function validateFieldRules(name, definition, value, errors, mode, _context) {
134
135
  const href = link.href.trim();
135
136
  if (mode === "publish" && options.required && !href) addError(errors, name, "This field is required.");
136
137
  if (!href) return;
137
- if (link.type === "external" && !urlPattern.test(href)) addError(errors, name, "External links must be HTTP(S) URLs.");
138
+ if (link.type === "external" && !isAllowedProtocol(href, definition.options.protocols)) {
139
+ const allowed = normalizeProtocols(definition.options.protocols);
140
+ addError(errors, name, isDefaultProtocols(definition.options.protocols) ? "External links must be HTTP(S) URLs." : `External links must start with ${allowed.map((protocol) => `${protocol}:`).join(" or ")}.`);
141
+ }
138
142
  if (link.type === "internal" && !href.startsWith("/") && !href.startsWith("#")) addError(errors, name, "Internal links must start with / or #.");
139
143
  return;
140
144
  }
@@ -160,6 +164,16 @@ function validateFieldRules(name, definition, value, errors, mode, _context) {
160
164
  if (definition.options.max && normalized > definition.options.max) addError(errors, name, `Must be on or before ${definition.options.max}.`);
161
165
  return;
162
166
  }
167
+ if (definition.type === "phone") {
168
+ const phone = normalizeEponymePhone(normalized, definition.options);
169
+ if (normalized && phone.countryNotAllowed) {
170
+ const accepted = definition.options.countries?.join(", ");
171
+ addError(errors, name, phone.country ? `Numbers from ${phone.country} are not accepted. Use a number from: ${accepted}.` : `Must be a number from: ${accepted}.`);
172
+ } else if (normalized && !phone.valid) {
173
+ addError(errors, name, definition.options.detectCountry === false ? "Must be a valid phone number in international format, starting with the country code." : "Must be a valid phone number.");
174
+ }
175
+ return;
176
+ }
163
177
  if (definition.type === "color") {
164
178
  if (normalized && !colorPattern.test(normalized)) addError(errors, name, "Must be a valid hex color.");
165
179
  return;
@@ -179,8 +193,20 @@ function validateFieldRules(name, definition, value, errors, mode, _context) {
179
193
  if (definition.options.maxLength !== void 0 && value.length > definition.options.maxLength) addError(errors, name, `Must contain at most ${definition.options.maxLength} characters.`);
180
194
  if (definition.options.regex && !definition.options.regex.test(value)) addError(errors, name, "Has an invalid format.");
181
195
  const [localPart, domain, ...extraParts] = normalized.split("@");
182
- if ((definition.type === "email" || definition.options.email) && normalized && (!localPart || !domain?.includes(".") || extraParts.length)) addError(errors, name, "Must be a valid email address.");
183
- if (definition.options.url && normalized && !urlPattern.test(normalized)) addError(errors, name, "Must be an HTTP(S) URL.");
196
+ if (definition.type === "email" && normalized && (!localPart || !domain?.includes(".") || extraParts.length)) addError(errors, name, "Must be a valid email address.");
197
+ }
198
+ const DEFAULT_URL_PROTOCOLS = ["http", "https"];
199
+ const schemePattern = /^([a-z][a-z0-9+.-]*):/i;
200
+ function normalizeProtocols(protocols) {
201
+ const declared = protocols?.length ? protocols : DEFAULT_URL_PROTOCOLS;
202
+ return declared.map((protocol) => protocol.trim().toLowerCase().replace(/:.*$/, ""));
203
+ }
204
+ function isDefaultProtocols(protocols) {
205
+ return !protocols?.length;
206
+ }
207
+ function isAllowedProtocol(href, protocols) {
208
+ const scheme = schemePattern.exec(href)?.[1]?.toLowerCase();
209
+ return Boolean(scheme) && normalizeProtocols(protocols).includes(scheme);
184
210
  }
185
211
  function getRichTextContent(value) {
186
212
  return value.replace(/<br\s*\/?>/gi, " ").replace(/<\/p>|<\/h[1-6]>|<\/li>|<\/blockquote>/gi, " ").replace(/<[^>]+>/g, "").replace(/&nbsp;|&#160;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/\s+/g, " ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karibsen/eponyme",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A lightweight, type-safe content manager for Nuxt.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,9 +60,9 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@iconify-json/mingcute": "^1.2.7",
63
+ "@karibsen/ui": "latest",
63
64
  "@nuxt/icon": "^2.3.1",
64
65
  "@nuxt/kit": "^4.4.8",
65
- "@karibsen/ui": "latest",
66
66
  "@tailwindcss/vite": "^4.1.12",
67
67
  "@tanstack/vue-table": "^8.21.3",
68
68
  "@tiptap/extension-image": "^3.29.2",
@@ -72,6 +72,7 @@
72
72
  "@vueuse/core": "^14.3.0",
73
73
  "@vueuse/integrations": "^14.3.0",
74
74
  "fuse.js": "^7.5.0",
75
+ "libphonenumber-js": "^1.13.9",
75
76
  "picocolors": "^1.1.1",
76
77
  "reka-ui": "^2.10.1",
77
78
  "slugify": "^1.6.9",
@@ -1,6 +0,0 @@
1
- import type { H3Event } from 'h3';
2
- import type { EponymeCaptchaResult, EponymeCaptchaVerifier } from '../../types/captcha.js';
3
- export declare const CAPTCHA_TOKEN_KEY = "_eponyme_captcha";
4
- export declare function eponymeCaptchaVerifier(): EponymeCaptchaVerifier;
5
- export declare function isEponymeCaptchaConfigured(): boolean;
6
- export declare function verifyEponymeCaptcha(event: H3Event, token: unknown): Promise<EponymeCaptchaResult>;
@@ -1,14 +0,0 @@
1
- import { getRequestIP } from "h3";
2
- import captchaVerifier from "#eponyme/captcha";
3
- export const CAPTCHA_TOKEN_KEY = "_eponyme_captcha";
4
- export function eponymeCaptchaVerifier() {
5
- return captchaVerifier;
6
- }
7
- export function isEponymeCaptchaConfigured() {
8
- return eponymeCaptchaVerifier().name !== "none";
9
- }
10
- export async function verifyEponymeCaptcha(event, token) {
11
- if (typeof token !== "string" || !token.trim())
12
- return { success: false, reason: "Missing captcha token." };
13
- return eponymeCaptchaVerifier().verify(token, { ip: getRequestIP(event, { xForwardedFor: true }) });
14
- }
@@ -1,16 +0,0 @@
1
- export interface EponymeCaptchaResult {
2
- success: boolean;
3
- /** Provider-specific reason, surfaced in server logs rather than to the visitor. */
4
- reason?: string;
5
- }
6
- /**
7
- * Verifies the token a visitor's captcha widget produced. Implemented by an adapter
8
- * package such as `@karibsen/eponyme-captcha`; the core ships none, so a project
9
- * without an adapter simply has no captcha.
10
- */
11
- export interface EponymeCaptchaVerifier {
12
- name: string;
13
- verify: (token: string, context: {
14
- ip?: string;
15
- }) => Promise<EponymeCaptchaResult>;
16
- }
File without changes
@@ -1,7 +0,0 @@
1
- import type { EponymeCaptchaVerifier } from '../types/captcha.js';
2
- /**
3
- * Stands in when no adapter is installed. It never claims success, so a form that
4
- * requires a captcha refuses submissions rather than silently accepting them all.
5
- */
6
- declare const verifier: EponymeCaptchaVerifier;
7
- export default verifier;
@@ -1,5 +0,0 @@
1
- const verifier = {
2
- name: "none",
3
- verify: async () => ({ success: false, reason: "No captcha adapter is installed." })
4
- };
5
- export default verifier;