@fewchurs/next 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,41 @@
1
+ Fewchurs SDK License
2
+
3
+ Copyright (c) 2026 Fewchurs. All rights reserved.
4
+
5
+ This software and its source code (the "SDK") are proprietary to Fewchurs.
6
+ The source code is published for transparency only. It is not open source.
7
+
8
+ 1. Permitted use. You may install the SDK, unmodified, as an npm
9
+ dependency of your own applications, and deploy it as part of those
10
+ applications, including in the bundles they serve to browsers, solely to
11
+ connect them to the Fewchurs service under a valid Fewchurs account. You
12
+ may customise its look and text through the props, CSS variables, slots,
13
+ hooks and stylesheet overrides the SDK documents.
14
+
15
+ 2. Restrictions. Except as allowed in section 1, you may not:
16
+ a. copy, modify, translate or create derivative works of the SDK;
17
+ b. distribute, sublicense, sell, rent or lease the SDK on its own or as
18
+ part of a library, SDK, framework or developer tool;
19
+ c. use the SDK, or any part of it, to build or run a product or service
20
+ that competes with Fewchurs, or to connect to any service other than
21
+ Fewchurs;
22
+ d. remove or alter any copyright or license notice in the SDK.
23
+
24
+ 3. Ownership. Fewchurs keeps all rights, title and interest in the SDK. No
25
+ rights are granted except those stated in this license.
26
+
27
+ 4. Termination. This license ends automatically if you breach it, or when
28
+ your Fewchurs account ends. On termination you must stop deploying new
29
+ versions of your applications that include the SDK.
30
+
31
+ 5. No warranty. THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
34
+
35
+ 6. Limitation of liability. IN NO EVENT SHALL FEWCHURS BE LIABLE FOR ANY
36
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
37
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SDK OR THE USE
38
+ OF OR OTHER DEALINGS IN THE SDK.
39
+
40
+ Your use of the Fewchurs service itself is governed separately by your
41
+ agreement with Fewchurs (https://www.fewchurs.com).
package/README.md CHANGED
@@ -1,3 +1,34 @@
1
- # Temporary Holding Version
1
+ # @fewchurs/next
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A server-rendered Fewchurs feature-request board for the Next.js App Router. The API key stays
4
+ on the server. See the [repository README](../../README.md) for the full guide.
5
+
6
+ ```bash
7
+ pnpm add @fewchurs/next
8
+ ```
9
+
10
+ ```dotenv
11
+ FEWCHURS_API_KEY=fr_live_…
12
+ ```
13
+
14
+ ```ts
15
+ // app/api/fewchurs/[...route]/route.ts
16
+ export { GET, POST, DELETE } from "@fewchurs/next/handler";
17
+ ```
18
+
19
+ ```tsx
20
+ // app/feedback/page.tsx
21
+ import { FewchursBoard } from "@fewchurs/next";
22
+ import "@fewchurs/next/styles.css";
23
+
24
+ export default function Page() {
25
+ return <FewchursBoard />;
26
+ }
27
+ ```
28
+
29
+ - `<FewchursBoard initialFeatureId="…">` — opens the board on one request, rendered on the
30
+ server, for a link straight to it.
31
+ - `createHandler({ apiKey, baseUrl, apiPath, deviceId })` — the proxy, mounted wherever you like.
32
+ - `@fewchurs/next/actions` — `submitFeatureAction`, `voteAction`, `commentAction`, `followAction`
33
+ for boards built from plain forms.
34
+ - `@fewchurs/next/identity` — the `httpOnly` device-id cookie helpers.
@@ -0,0 +1,14 @@
1
+ interface ActionResult {
2
+ ok: boolean;
3
+ message?: string;
4
+ }
5
+ /**
6
+ * Server actions for boards built with plain forms — the client board talks to the route
7
+ * handler instead. Pass `path` when the page should re-render with the new data.
8
+ */
9
+ declare function submitFeatureAction(formData: FormData): Promise<ActionResult>;
10
+ declare function voteAction(formData: FormData): Promise<ActionResult>;
11
+ declare function commentAction(formData: FormData): Promise<ActionResult>;
12
+ declare function followAction(formData: FormData): Promise<ActionResult>;
13
+
14
+ export { type ActionResult, commentAction, followAction, submitFeatureAction, voteAction };
@@ -0,0 +1,14 @@
1
+ interface ActionResult {
2
+ ok: boolean;
3
+ message?: string;
4
+ }
5
+ /**
6
+ * Server actions for boards built with plain forms — the client board talks to the route
7
+ * handler instead. Pass `path` when the page should re-render with the new data.
8
+ */
9
+ declare function submitFeatureAction(formData: FormData): Promise<ActionResult>;
10
+ declare function voteAction(formData: FormData): Promise<ActionResult>;
11
+ declare function commentAction(formData: FormData): Promise<ActionResult>;
12
+ declare function followAction(formData: FormData): Promise<ActionResult>;
13
+
14
+ export { type ActionResult, commentAction, followAction, submitFeatureAction, voteAction };
@@ -0,0 +1,133 @@
1
+ "use server";
2
+ "use strict";
3
+ "use server";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/actions.ts
23
+ var actions_exports = {};
24
+ __export(actions_exports, {
25
+ commentAction: () => commentAction,
26
+ followAction: () => followAction,
27
+ submitFeatureAction: () => submitFeatureAction,
28
+ voteAction: () => voteAction
29
+ });
30
+ module.exports = __toCommonJS(actions_exports);
31
+ var import_cache = require("next/cache");
32
+ var import_core3 = require("@fewchurs/react/core");
33
+
34
+ // src/config.ts
35
+ var import_server_only = require("server-only");
36
+ var import_core = require("@fewchurs/react/core");
37
+ function resolveApiKey(apiKey) {
38
+ return apiKey ?? process.env.FEWCHURS_API_KEY;
39
+ }
40
+ function resolveBaseUrl(baseUrl) {
41
+ return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? import_core.DEFAULT_BASE_URL;
42
+ }
43
+ function serverClient(deviceId, config = {}) {
44
+ const apiKey = resolveApiKey(config.apiKey);
45
+ if (!apiKey) {
46
+ throw new Error(
47
+ "[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler()."
48
+ );
49
+ }
50
+ return (0, import_core.createFewchursClient)({
51
+ apiKey,
52
+ baseUrl: resolveBaseUrl(config.baseUrl),
53
+ deviceId,
54
+ language: config.language
55
+ });
56
+ }
57
+
58
+ // src/identity.ts
59
+ var import_server_only2 = require("server-only");
60
+ var import_headers = require("next/headers");
61
+ var import_core2 = require("@fewchurs/react/core");
62
+
63
+ // src/version.ts
64
+ var SDK_VERSION = "1.0.1";
65
+
66
+ // src/constants.ts
67
+ var DEVICE_ID_COOKIE = "fewchurs_did";
68
+ var DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;
69
+ var SDK_HEADER = `next/${SDK_VERSION}`;
70
+
71
+ // src/identity.ts
72
+ async function readDeviceId() {
73
+ const store = await (0, import_headers.cookies)();
74
+ return store.get(DEVICE_ID_COOKIE)?.value ?? null;
75
+ }
76
+ async function resolveDeviceId(custom) {
77
+ if (custom) return custom();
78
+ return await readDeviceId() ?? (0, import_core2.createDeviceId)();
79
+ }
80
+
81
+ // src/actions.ts
82
+ async function submitFeatureAction(formData) {
83
+ const client = serverClient(await resolveDeviceId());
84
+ const result = await client.submitFeature({
85
+ title: String(formData.get("title") ?? ""),
86
+ description: String(formData.get("description") ?? ""),
87
+ email: optional(formData.get("email"))
88
+ });
89
+ revalidate(formData);
90
+ return result.ok ? { ok: true } : { ok: false, message: (0, import_core3.userMessage)(result.error) };
91
+ }
92
+ async function voteAction(formData) {
93
+ const featureId = String(formData.get("featureId") ?? "");
94
+ const hasVoted = formData.get("hasVoted") === "true";
95
+ const client = serverClient(await resolveDeviceId());
96
+ const result = hasVoted ? await client.unvote(featureId) : await client.vote(featureId);
97
+ revalidate(formData);
98
+ return result.ok ? { ok: true } : { ok: false, message: (0, import_core3.userMessage)(result.error) };
99
+ }
100
+ async function commentAction(formData) {
101
+ const client = serverClient(await resolveDeviceId());
102
+ const result = await client.createComment({
103
+ featureId: String(formData.get("featureId") ?? ""),
104
+ text: String(formData.get("text") ?? ""),
105
+ authorName: optional(formData.get("authorName"))
106
+ });
107
+ revalidate(formData);
108
+ return result.ok ? { ok: true } : { ok: false, message: (0, import_core3.userMessage)(result.error) };
109
+ }
110
+ async function followAction(formData) {
111
+ const featureId = String(formData.get("featureId") ?? "");
112
+ const email = optional(formData.get("email"));
113
+ const client = serverClient(await resolveDeviceId());
114
+ const result = email ? await client.follow(featureId, email) : await client.unfollow(featureId);
115
+ revalidate(formData);
116
+ return result.ok ? { ok: true } : { ok: false, message: (0, import_core3.userMessage)(result.error) };
117
+ }
118
+ function optional(value) {
119
+ const text = typeof value === "string" ? value.trim() : "";
120
+ return text.length > 0 ? text : void 0;
121
+ }
122
+ function revalidate(formData) {
123
+ const path = formData.get("path");
124
+ if (typeof path === "string" && path.startsWith("/")) (0, import_cache.revalidatePath)(path);
125
+ }
126
+ // Annotate the CommonJS export names for ESM import in node:
127
+ 0 && (module.exports = {
128
+ commentAction,
129
+ followAction,
130
+ submitFeatureAction,
131
+ voteAction
132
+ });
133
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/actions.ts","../src/config.ts","../src/identity.ts","../src/version.ts","../src/constants.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { userMessage } from \"@fewchurs/react/core\";\nimport { serverClient } from \"./config\";\nimport { resolveDeviceId } from \"./identity\";\n\nexport interface ActionResult {\n ok: boolean;\n message?: string;\n}\n\n/**\n * Server actions for boards built with plain forms — the client board talks to the route\n * handler instead. Pass `path` when the page should re-render with the new data.\n */\nexport async function submitFeatureAction(formData: FormData): Promise<ActionResult> {\n const client = serverClient(await resolveDeviceId());\n const result = await client.submitFeature({\n title: String(formData.get(\"title\") ?? \"\"),\n description: String(formData.get(\"description\") ?? \"\"),\n email: optional(formData.get(\"email\")),\n });\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function voteAction(formData: FormData): Promise<ActionResult> {\n const featureId = String(formData.get(\"featureId\") ?? \"\");\n const hasVoted = formData.get(\"hasVoted\") === \"true\";\n const client = serverClient(await resolveDeviceId());\n const result = hasVoted ? await client.unvote(featureId) : await client.vote(featureId);\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function commentAction(formData: FormData): Promise<ActionResult> {\n const client = serverClient(await resolveDeviceId());\n const result = await client.createComment({\n featureId: String(formData.get(\"featureId\") ?? \"\"),\n text: String(formData.get(\"text\") ?? \"\"),\n authorName: optional(formData.get(\"authorName\")),\n });\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function followAction(formData: FormData): Promise<ActionResult> {\n const featureId = String(formData.get(\"featureId\") ?? \"\");\n const email = optional(formData.get(\"email\"));\n const client = serverClient(await resolveDeviceId());\n const result = email ? await client.follow(featureId, email) : await client.unfollow(featureId);\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nfunction optional(value: FormDataEntryValue | null): string | undefined {\n const text = typeof value === \"string\" ? value.trim() : \"\";\n return text.length > 0 ? text : undefined;\n}\n\nfunction revalidate(formData: FormData): void {\n const path = formData.get(\"path\");\n if (typeof path === \"string\" && path.startsWith(\"/\")) revalidatePath(path);\n}\n","import \"server-only\";\nimport { createFewchursClient, DEFAULT_BASE_URL, type FewchursClient } from \"@fewchurs/react/core\";\n\nexport const DEFAULT_API_PATH = \"/api/fewchurs\";\n\nexport interface ServerConfig {\n apiKey?: string;\n baseUrl?: string;\n language?: string;\n}\n\nexport function resolveApiKey(apiKey?: string): string | undefined {\n return apiKey ?? process.env.FEWCHURS_API_KEY;\n}\n\nexport function resolveBaseUrl(baseUrl?: string): string {\n return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? DEFAULT_BASE_URL;\n}\n\nexport function resolveApiPath(apiPath?: string): string {\n return (apiPath ?? process.env.FEWCHURS_API_PATH ?? DEFAULT_API_PATH).replace(/\\/$/, \"\");\n}\n\nexport function serverClient(deviceId: string | null, config: ServerConfig = {}): FewchursClient {\n const apiKey = resolveApiKey(config.apiKey);\n if (!apiKey) {\n throw new Error(\n \"[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler().\",\n );\n }\n\n return createFewchursClient({\n apiKey,\n baseUrl: resolveBaseUrl(config.baseUrl),\n deviceId,\n language: config.language,\n });\n}\n","import \"server-only\";\nimport { cookies } from \"next/headers\";\nimport { createDeviceId } from \"@fewchurs/react/core\";\nimport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE } from \"./constants\";\n\nexport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE };\n\nexport type DeviceIdResolver = () => string | Promise<string>;\n\n/** The visitor's id, or null when nothing has been stored for them yet. */\nexport async function readDeviceId(): Promise<string | null> {\n const store = await cookies();\n return store.get(DEVICE_ID_COOKIE)?.value ?? null;\n}\n\n/**\n * Reads the cookie, falling back to a fresh id. The value is not persisted here: a server\n * component cannot set cookies, so the route handler stores it on the first request instead.\n */\nexport async function resolveDeviceId(custom?: DeviceIdResolver): Promise<string> {\n if (custom) return custom();\n return (await readDeviceId()) ?? createDeviceId();\n}\n\nexport function deviceIdCookie(deviceId: string): {\n name: string;\n value: string;\n options: {\n httpOnly: true;\n sameSite: \"lax\";\n path: string;\n maxAge: number;\n secure: boolean;\n };\n} {\n return {\n name: DEVICE_ID_COOKIE,\n value: deviceId,\n options: {\n httpOnly: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: DEVICE_ID_MAX_AGE,\n secure: process.env.NODE_ENV === \"production\",\n },\n };\n}\n\nexport { createDeviceId };\n","// Generated by tools/stamp-version.mjs from @fewchurs/next's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import { SDK_VERSION } from \"./version\";\n\nexport const DEVICE_ID_COOKIE = \"fewchurs_did\";\nexport const DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;\nexport { SDK_VERSION };\nexport const SDK_HEADER = `next/${SDK_VERSION}`;\n"],"mappings":"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","names":["import_core","import_server_only","import_core"]}
@@ -0,0 +1,106 @@
1
+ "use server";
2
+ "use server";
3
+
4
+ // src/actions.ts
5
+ import { revalidatePath } from "next/cache";
6
+ import { userMessage } from "@fewchurs/react/core";
7
+
8
+ // src/config.ts
9
+ import "server-only";
10
+ import { createFewchursClient, DEFAULT_BASE_URL } from "@fewchurs/react/core";
11
+ function resolveApiKey(apiKey) {
12
+ return apiKey ?? process.env.FEWCHURS_API_KEY;
13
+ }
14
+ function resolveBaseUrl(baseUrl) {
15
+ return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? DEFAULT_BASE_URL;
16
+ }
17
+ function serverClient(deviceId, config = {}) {
18
+ const apiKey = resolveApiKey(config.apiKey);
19
+ if (!apiKey) {
20
+ throw new Error(
21
+ "[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler()."
22
+ );
23
+ }
24
+ return createFewchursClient({
25
+ apiKey,
26
+ baseUrl: resolveBaseUrl(config.baseUrl),
27
+ deviceId,
28
+ language: config.language
29
+ });
30
+ }
31
+
32
+ // src/identity.ts
33
+ import "server-only";
34
+ import { cookies } from "next/headers";
35
+ import { createDeviceId } from "@fewchurs/react/core";
36
+
37
+ // src/version.ts
38
+ var SDK_VERSION = "1.0.1";
39
+
40
+ // src/constants.ts
41
+ var DEVICE_ID_COOKIE = "fewchurs_did";
42
+ var DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;
43
+ var SDK_HEADER = `next/${SDK_VERSION}`;
44
+
45
+ // src/identity.ts
46
+ async function readDeviceId() {
47
+ const store = await cookies();
48
+ return store.get(DEVICE_ID_COOKIE)?.value ?? null;
49
+ }
50
+ async function resolveDeviceId(custom) {
51
+ if (custom) return custom();
52
+ return await readDeviceId() ?? createDeviceId();
53
+ }
54
+
55
+ // src/actions.ts
56
+ async function submitFeatureAction(formData) {
57
+ const client = serverClient(await resolveDeviceId());
58
+ const result = await client.submitFeature({
59
+ title: String(formData.get("title") ?? ""),
60
+ description: String(formData.get("description") ?? ""),
61
+ email: optional(formData.get("email"))
62
+ });
63
+ revalidate(formData);
64
+ return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };
65
+ }
66
+ async function voteAction(formData) {
67
+ const featureId = String(formData.get("featureId") ?? "");
68
+ const hasVoted = formData.get("hasVoted") === "true";
69
+ const client = serverClient(await resolveDeviceId());
70
+ const result = hasVoted ? await client.unvote(featureId) : await client.vote(featureId);
71
+ revalidate(formData);
72
+ return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };
73
+ }
74
+ async function commentAction(formData) {
75
+ const client = serverClient(await resolveDeviceId());
76
+ const result = await client.createComment({
77
+ featureId: String(formData.get("featureId") ?? ""),
78
+ text: String(formData.get("text") ?? ""),
79
+ authorName: optional(formData.get("authorName"))
80
+ });
81
+ revalidate(formData);
82
+ return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };
83
+ }
84
+ async function followAction(formData) {
85
+ const featureId = String(formData.get("featureId") ?? "");
86
+ const email = optional(formData.get("email"));
87
+ const client = serverClient(await resolveDeviceId());
88
+ const result = email ? await client.follow(featureId, email) : await client.unfollow(featureId);
89
+ revalidate(formData);
90
+ return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };
91
+ }
92
+ function optional(value) {
93
+ const text = typeof value === "string" ? value.trim() : "";
94
+ return text.length > 0 ? text : void 0;
95
+ }
96
+ function revalidate(formData) {
97
+ const path = formData.get("path");
98
+ if (typeof path === "string" && path.startsWith("/")) revalidatePath(path);
99
+ }
100
+ export {
101
+ commentAction,
102
+ followAction,
103
+ submitFeatureAction,
104
+ voteAction
105
+ };
106
+ //# sourceMappingURL=actions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/actions.ts","../src/config.ts","../src/identity.ts","../src/version.ts","../src/constants.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { userMessage } from \"@fewchurs/react/core\";\nimport { serverClient } from \"./config\";\nimport { resolveDeviceId } from \"./identity\";\n\nexport interface ActionResult {\n ok: boolean;\n message?: string;\n}\n\n/**\n * Server actions for boards built with plain forms — the client board talks to the route\n * handler instead. Pass `path` when the page should re-render with the new data.\n */\nexport async function submitFeatureAction(formData: FormData): Promise<ActionResult> {\n const client = serverClient(await resolveDeviceId());\n const result = await client.submitFeature({\n title: String(formData.get(\"title\") ?? \"\"),\n description: String(formData.get(\"description\") ?? \"\"),\n email: optional(formData.get(\"email\")),\n });\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function voteAction(formData: FormData): Promise<ActionResult> {\n const featureId = String(formData.get(\"featureId\") ?? \"\");\n const hasVoted = formData.get(\"hasVoted\") === \"true\";\n const client = serverClient(await resolveDeviceId());\n const result = hasVoted ? await client.unvote(featureId) : await client.vote(featureId);\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function commentAction(formData: FormData): Promise<ActionResult> {\n const client = serverClient(await resolveDeviceId());\n const result = await client.createComment({\n featureId: String(formData.get(\"featureId\") ?? \"\"),\n text: String(formData.get(\"text\") ?? \"\"),\n authorName: optional(formData.get(\"authorName\")),\n });\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nexport async function followAction(formData: FormData): Promise<ActionResult> {\n const featureId = String(formData.get(\"featureId\") ?? \"\");\n const email = optional(formData.get(\"email\"));\n const client = serverClient(await resolveDeviceId());\n const result = email ? await client.follow(featureId, email) : await client.unfollow(featureId);\n\n revalidate(formData);\n return result.ok ? { ok: true } : { ok: false, message: userMessage(result.error) };\n}\n\nfunction optional(value: FormDataEntryValue | null): string | undefined {\n const text = typeof value === \"string\" ? value.trim() : \"\";\n return text.length > 0 ? text : undefined;\n}\n\nfunction revalidate(formData: FormData): void {\n const path = formData.get(\"path\");\n if (typeof path === \"string\" && path.startsWith(\"/\")) revalidatePath(path);\n}\n","import \"server-only\";\nimport { createFewchursClient, DEFAULT_BASE_URL, type FewchursClient } from \"@fewchurs/react/core\";\n\nexport const DEFAULT_API_PATH = \"/api/fewchurs\";\n\nexport interface ServerConfig {\n apiKey?: string;\n baseUrl?: string;\n language?: string;\n}\n\nexport function resolveApiKey(apiKey?: string): string | undefined {\n return apiKey ?? process.env.FEWCHURS_API_KEY;\n}\n\nexport function resolveBaseUrl(baseUrl?: string): string {\n return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? DEFAULT_BASE_URL;\n}\n\nexport function resolveApiPath(apiPath?: string): string {\n return (apiPath ?? process.env.FEWCHURS_API_PATH ?? DEFAULT_API_PATH).replace(/\\/$/, \"\");\n}\n\nexport function serverClient(deviceId: string | null, config: ServerConfig = {}): FewchursClient {\n const apiKey = resolveApiKey(config.apiKey);\n if (!apiKey) {\n throw new Error(\n \"[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler().\",\n );\n }\n\n return createFewchursClient({\n apiKey,\n baseUrl: resolveBaseUrl(config.baseUrl),\n deviceId,\n language: config.language,\n });\n}\n","import \"server-only\";\nimport { cookies } from \"next/headers\";\nimport { createDeviceId } from \"@fewchurs/react/core\";\nimport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE } from \"./constants\";\n\nexport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE };\n\nexport type DeviceIdResolver = () => string | Promise<string>;\n\n/** The visitor's id, or null when nothing has been stored for them yet. */\nexport async function readDeviceId(): Promise<string | null> {\n const store = await cookies();\n return store.get(DEVICE_ID_COOKIE)?.value ?? null;\n}\n\n/**\n * Reads the cookie, falling back to a fresh id. The value is not persisted here: a server\n * component cannot set cookies, so the route handler stores it on the first request instead.\n */\nexport async function resolveDeviceId(custom?: DeviceIdResolver): Promise<string> {\n if (custom) return custom();\n return (await readDeviceId()) ?? createDeviceId();\n}\n\nexport function deviceIdCookie(deviceId: string): {\n name: string;\n value: string;\n options: {\n httpOnly: true;\n sameSite: \"lax\";\n path: string;\n maxAge: number;\n secure: boolean;\n };\n} {\n return {\n name: DEVICE_ID_COOKIE,\n value: deviceId,\n options: {\n httpOnly: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: DEVICE_ID_MAX_AGE,\n secure: process.env.NODE_ENV === \"production\",\n },\n };\n}\n\nexport { createDeviceId };\n","// Generated by tools/stamp-version.mjs from @fewchurs/next's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import { SDK_VERSION } from \"./version\";\n\nexport const DEVICE_ID_COOKIE = \"fewchurs_did\";\nexport const DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;\nexport { SDK_VERSION };\nexport const SDK_HEADER = `next/${SDK_VERSION}`;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,145 @@
1
+ import {
2
+ DEVICE_ID_COOKIE,
3
+ DEVICE_ID_MAX_AGE,
4
+ SDK_HEADER
5
+ } from "./chunk-W6QR6DEB.mjs";
6
+
7
+ // src/handler.ts
8
+ import "server-only";
9
+ import { createDeviceId } from "@fewchurs/react/core";
10
+
11
+ // src/config.ts
12
+ import "server-only";
13
+ import { createFewchursClient, DEFAULT_BASE_URL } from "@fewchurs/react/core";
14
+ var DEFAULT_API_PATH = "/api/fewchurs";
15
+ function resolveApiKey(apiKey) {
16
+ return apiKey ?? process.env.FEWCHURS_API_KEY;
17
+ }
18
+ function resolveBaseUrl(baseUrl) {
19
+ return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? DEFAULT_BASE_URL;
20
+ }
21
+ function resolveApiPath(apiPath) {
22
+ return (apiPath ?? process.env.FEWCHURS_API_PATH ?? DEFAULT_API_PATH).replace(/\/$/, "");
23
+ }
24
+ function serverClient(deviceId, config = {}) {
25
+ const apiKey = resolveApiKey(config.apiKey);
26
+ if (!apiKey) {
27
+ throw new Error(
28
+ "[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler()."
29
+ );
30
+ }
31
+ return createFewchursClient({
32
+ apiKey,
33
+ baseUrl: resolveBaseUrl(config.baseUrl),
34
+ deviceId,
35
+ language: config.language
36
+ });
37
+ }
38
+
39
+ // src/handler.ts
40
+ var ALLOWED = [
41
+ { method: "GET", pattern: /^config$/ },
42
+ { method: "GET", pattern: /^features$/ },
43
+ { method: "POST", pattern: /^features$/ },
44
+ { method: "GET", pattern: /^features\/[^/]+$/ },
45
+ { method: "POST", pattern: /^features\/[^/]+\/vote$/ },
46
+ { method: "DELETE", pattern: /^features\/[^/]+\/vote$/ },
47
+ { method: "POST", pattern: /^features\/[^/]+\/follow$/ },
48
+ { method: "DELETE", pattern: /^features\/[^/]+\/follow$/ },
49
+ { method: "GET", pattern: /^features\/[^/]+\/comments$/ },
50
+ { method: "POST", pattern: /^comments$/ }
51
+ ];
52
+ var DEVICE_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
53
+ function createHandler(options = {}) {
54
+ async function handle(request, context) {
55
+ const path = await routePath(request, context, resolveApiPath(options.apiPath));
56
+ if (!ALLOWED.some((route) => route.method === request.method && route.pattern.test(path))) {
57
+ return json({ error: { code: "not_found", message: "Unknown Fewchurs route." } }, 404);
58
+ }
59
+ const apiKey = resolveApiKey(options.apiKey);
60
+ if (!apiKey) {
61
+ console.error(
62
+ "[fewchurs] No API key. Set FEWCHURS_API_KEY, or pass apiKey to createHandler()."
63
+ );
64
+ return json({ error: { code: "internal", message: "Fewchurs is not configured." } }, 500);
65
+ }
66
+ const cookieValue = readCookie(request, DEVICE_ID_COOKIE);
67
+ const custom = options.deviceId ? await options.deviceId() : null;
68
+ const deviceId = custom ?? cookieValue ?? claimedDeviceId(request) ?? createDeviceId();
69
+ const target = `${resolveBaseUrl(options.baseUrl)}/${path}${new URL(request.url).search}`;
70
+ const headers = {
71
+ Authorization: `Bearer ${apiKey}`,
72
+ "X-Device-Id": deviceId,
73
+ "X-Fewchurs-Sdk": request.headers.get("x-fewchurs-sdk") ?? SDK_HEADER,
74
+ Accept: "application/json"
75
+ };
76
+ const language = request.headers.get("accept-language");
77
+ if (language) headers["Accept-Language"] = language;
78
+ const body = request.method === "GET" ? void 0 : await request.text();
79
+ if (body) headers["Content-Type"] = "application/json";
80
+ let upstream;
81
+ try {
82
+ upstream = await fetch(target, { method: request.method, headers, body, cache: "no-store" });
83
+ } catch {
84
+ return json({ error: { code: "internal", message: "Could not reach Fewchurs." } }, 502);
85
+ }
86
+ const response = new Response(await upstream.text(), {
87
+ status: upstream.status,
88
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
89
+ });
90
+ if (!custom && deviceId !== cookieValue) {
91
+ response.headers.append("Set-Cookie", serializeCookie(deviceId));
92
+ }
93
+ return response;
94
+ }
95
+ return { GET: handle, POST: handle, DELETE: handle };
96
+ }
97
+ var defaultHandler = createHandler();
98
+ var GET = defaultHandler.GET;
99
+ var POST = defaultHandler.POST;
100
+ var DELETE = defaultHandler.DELETE;
101
+ async function routePath(request, context, apiPath) {
102
+ const params = await context?.params;
103
+ if (params?.route?.length) return params.route.map(encodeURIComponent).join("/");
104
+ const { pathname } = new URL(request.url);
105
+ const index = pathname.indexOf(apiPath);
106
+ return index === -1 ? "" : pathname.slice(index + apiPath.length).replace(/^\//, "");
107
+ }
108
+ function claimedDeviceId(request) {
109
+ const claimed = request.headers.get("x-device-id");
110
+ return claimed && DEVICE_ID_PATTERN.test(claimed) ? claimed : null;
111
+ }
112
+ function readCookie(request, name) {
113
+ const header = request.headers.get("cookie");
114
+ if (!header) return null;
115
+ const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
116
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
117
+ }
118
+ function serializeCookie(deviceId) {
119
+ const parts = [
120
+ `${DEVICE_ID_COOKIE}=${encodeURIComponent(deviceId)}`,
121
+ "Path=/",
122
+ `Max-Age=${DEVICE_ID_MAX_AGE}`,
123
+ "HttpOnly",
124
+ "SameSite=Lax"
125
+ ];
126
+ if (process.env.NODE_ENV === "production") parts.push("Secure");
127
+ return parts.join("; ");
128
+ }
129
+ function json(body, status) {
130
+ return new Response(JSON.stringify(body), {
131
+ status,
132
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
133
+ });
134
+ }
135
+
136
+ export {
137
+ DEFAULT_API_PATH,
138
+ resolveApiPath,
139
+ serverClient,
140
+ createHandler,
141
+ GET,
142
+ POST,
143
+ DELETE
144
+ };
145
+ //# sourceMappingURL=chunk-KPJHLDQU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handler.ts","../src/config.ts"],"sourcesContent":["import \"server-only\";\nimport { createDeviceId } from \"@fewchurs/react/core\";\nimport { resolveApiKey, resolveApiPath, resolveBaseUrl } from \"./config\";\nimport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE, SDK_HEADER } from \"./constants\";\nimport type { DeviceIdResolver } from \"./identity\";\n\nexport interface HandlerOptions {\n apiKey?: string;\n baseUrl?: string;\n /** Where the handler is mounted, when it is not `/api/fewchurs`. */\n apiPath?: string;\n /** Return a stable, hashed id for the signed-in visitor so votes follow the account. */\n deviceId?: DeviceIdResolver;\n}\n\ntype RouteParams = { route?: string[] };\n\n/** What Next passes: a Promise in 15, a plain object in 14. Both are awaited the same way. */\ntype IncomingContext = { params?: RouteParams | Promise<RouteParams> };\n\n// Next 15's route check wants a required second argument whose `params` is a Promise. The\n// handler itself takes `IncomingContext`, so Next 14's plain object still works at runtime.\ntype RouteContext = { params: Promise<RouteParams> };\n\nexport type RouteHandler = (request: Request, context: RouteContext) => Promise<Response>;\n\n/** Only the endpoints the board needs: everything else is a 404, not a proxy to the API. */\nconst ALLOWED: Array<{ method: string; pattern: RegExp }> = [\n { method: \"GET\", pattern: /^config$/ },\n { method: \"GET\", pattern: /^features$/ },\n { method: \"POST\", pattern: /^features$/ },\n { method: \"GET\", pattern: /^features\\/[^/]+$/ },\n { method: \"POST\", pattern: /^features\\/[^/]+\\/vote$/ },\n { method: \"DELETE\", pattern: /^features\\/[^/]+\\/vote$/ },\n { method: \"POST\", pattern: /^features\\/[^/]+\\/follow$/ },\n { method: \"DELETE\", pattern: /^features\\/[^/]+\\/follow$/ },\n { method: \"GET\", pattern: /^features\\/[^/]+\\/comments$/ },\n { method: \"POST\", pattern: /^comments$/ },\n];\n\nconst DEVICE_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;\n\nexport function createHandler(options: HandlerOptions = {}): {\n GET: RouteHandler;\n POST: RouteHandler;\n DELETE: RouteHandler;\n} {\n async function handle(request: Request, context?: IncomingContext): Promise<Response> {\n const path = await routePath(request, context, resolveApiPath(options.apiPath));\n\n if (!ALLOWED.some((route) => route.method === request.method && route.pattern.test(path))) {\n return json({ error: { code: \"not_found\", message: \"Unknown Fewchurs route.\" } }, 404);\n }\n\n const apiKey = resolveApiKey(options.apiKey);\n if (!apiKey) {\n console.error(\n \"[fewchurs] No API key. Set FEWCHURS_API_KEY, or pass apiKey to createHandler().\",\n );\n return json({ error: { code: \"internal\", message: \"Fewchurs is not configured.\" } }, 500);\n }\n\n const cookieValue = readCookie(request, DEVICE_ID_COOKIE);\n const custom = options.deviceId ? await options.deviceId() : null;\n const deviceId = custom ?? cookieValue ?? claimedDeviceId(request) ?? createDeviceId();\n\n const target = `${resolveBaseUrl(options.baseUrl)}/${path}${new URL(request.url).search}`;\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n \"X-Device-Id\": deviceId,\n \"X-Fewchurs-Sdk\": request.headers.get(\"x-fewchurs-sdk\") ?? SDK_HEADER,\n Accept: \"application/json\",\n };\n const language = request.headers.get(\"accept-language\");\n if (language) headers[\"Accept-Language\"] = language;\n\n const body = request.method === \"GET\" ? undefined : await request.text();\n if (body) headers[\"Content-Type\"] = \"application/json\";\n\n let upstream: Response;\n try {\n upstream = await fetch(target, { method: request.method, headers, body, cache: \"no-store\" });\n } catch {\n return json({ error: { code: \"internal\", message: \"Could not reach Fewchurs.\" } }, 502);\n }\n\n const response = new Response(await upstream.text(), {\n status: upstream.status,\n headers: { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" },\n });\n\n // A visitor keeps the same id across requests without the browser ever seeing the key,\n // and an httpOnly cookie means page scripts cannot read or change it either.\n if (!custom && deviceId !== cookieValue) {\n response.headers.append(\"Set-Cookie\", serializeCookie(deviceId));\n }\n\n return response;\n }\n\n return { GET: handle, POST: handle, DELETE: handle };\n}\n\nconst defaultHandler = createHandler();\n\nexport const GET = defaultHandler.GET;\nexport const POST = defaultHandler.POST;\nexport const DELETE = defaultHandler.DELETE;\n\nasync function routePath(\n request: Request,\n context: IncomingContext | undefined,\n apiPath: string,\n): Promise<string> {\n const params = await context?.params;\n if (params?.route?.length) return params.route.map(encodeURIComponent).join(\"/\");\n\n const { pathname } = new URL(request.url);\n const index = pathname.indexOf(apiPath);\n return index === -1 ? \"\" : pathname.slice(index + apiPath.length).replace(/^\\//, \"\");\n}\n\n/**\n * The first request from a server-rendered board carries the id the page was rendered with,\n * so adopting it keeps the server's `hasVoted` answers and the browser's in agreement.\n */\nfunction claimedDeviceId(request: Request): string | null {\n const claimed = request.headers.get(\"x-device-id\");\n return claimed && DEVICE_ID_PATTERN.test(claimed) ? claimed : null;\n}\n\nfunction readCookie(request: Request, name: string): string | null {\n const header = request.headers.get(\"cookie\");\n if (!header) return null;\n const match = header.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction serializeCookie(deviceId: string): string {\n const parts = [\n `${DEVICE_ID_COOKIE}=${encodeURIComponent(deviceId)}`,\n \"Path=/\",\n `Max-Age=${DEVICE_ID_MAX_AGE}`,\n \"HttpOnly\",\n \"SameSite=Lax\",\n ];\n if (process.env.NODE_ENV === \"production\") parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n\nfunction json(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" },\n });\n}\n","import \"server-only\";\nimport { createFewchursClient, DEFAULT_BASE_URL, type FewchursClient } from \"@fewchurs/react/core\";\n\nexport const DEFAULT_API_PATH = \"/api/fewchurs\";\n\nexport interface ServerConfig {\n apiKey?: string;\n baseUrl?: string;\n language?: string;\n}\n\nexport function resolveApiKey(apiKey?: string): string | undefined {\n return apiKey ?? process.env.FEWCHURS_API_KEY;\n}\n\nexport function resolveBaseUrl(baseUrl?: string): string {\n return baseUrl ?? process.env.FEWCHURS_BASE_URL ?? DEFAULT_BASE_URL;\n}\n\nexport function resolveApiPath(apiPath?: string): string {\n return (apiPath ?? process.env.FEWCHURS_API_PATH ?? DEFAULT_API_PATH).replace(/\\/$/, \"\");\n}\n\nexport function serverClient(deviceId: string | null, config: ServerConfig = {}): FewchursClient {\n const apiKey = resolveApiKey(config.apiKey);\n if (!apiKey) {\n throw new Error(\n \"[fewchurs] No API key. Set FEWCHURS_API_KEY in your environment, or pass apiKey to the server component or createHandler().\",\n );\n }\n\n return createFewchursClient({\n apiKey,\n baseUrl: resolveBaseUrl(config.baseUrl),\n deviceId,\n language: config.language,\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,38 @@
1
+ import {
2
+ DEVICE_ID_COOKIE,
3
+ DEVICE_ID_MAX_AGE
4
+ } from "./chunk-W6QR6DEB.mjs";
5
+
6
+ // src/identity.ts
7
+ import "server-only";
8
+ import { cookies } from "next/headers";
9
+ import { createDeviceId } from "@fewchurs/react/core";
10
+ async function readDeviceId() {
11
+ const store = await cookies();
12
+ return store.get(DEVICE_ID_COOKIE)?.value ?? null;
13
+ }
14
+ async function resolveDeviceId(custom) {
15
+ if (custom) return custom();
16
+ return await readDeviceId() ?? createDeviceId();
17
+ }
18
+ function deviceIdCookie(deviceId) {
19
+ return {
20
+ name: DEVICE_ID_COOKIE,
21
+ value: deviceId,
22
+ options: {
23
+ httpOnly: true,
24
+ sameSite: "lax",
25
+ path: "/",
26
+ maxAge: DEVICE_ID_MAX_AGE,
27
+ secure: process.env.NODE_ENV === "production"
28
+ }
29
+ };
30
+ }
31
+
32
+ export {
33
+ createDeviceId,
34
+ readDeviceId,
35
+ resolveDeviceId,
36
+ deviceIdCookie
37
+ };
38
+ //# sourceMappingURL=chunk-QULKPZB4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/identity.ts"],"sourcesContent":["import \"server-only\";\nimport { cookies } from \"next/headers\";\nimport { createDeviceId } from \"@fewchurs/react/core\";\nimport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE } from \"./constants\";\n\nexport { DEVICE_ID_COOKIE, DEVICE_ID_MAX_AGE };\n\nexport type DeviceIdResolver = () => string | Promise<string>;\n\n/** The visitor's id, or null when nothing has been stored for them yet. */\nexport async function readDeviceId(): Promise<string | null> {\n const store = await cookies();\n return store.get(DEVICE_ID_COOKIE)?.value ?? null;\n}\n\n/**\n * Reads the cookie, falling back to a fresh id. The value is not persisted here: a server\n * component cannot set cookies, so the route handler stores it on the first request instead.\n */\nexport async function resolveDeviceId(custom?: DeviceIdResolver): Promise<string> {\n if (custom) return custom();\n return (await readDeviceId()) ?? createDeviceId();\n}\n\nexport function deviceIdCookie(deviceId: string): {\n name: string;\n value: string;\n options: {\n httpOnly: true;\n sameSite: \"lax\";\n path: string;\n maxAge: number;\n secure: boolean;\n };\n} {\n return {\n name: DEVICE_ID_COOKIE,\n value: deviceId,\n options: {\n httpOnly: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: DEVICE_ID_MAX_AGE,\n secure: process.env.NODE_ENV === \"production\",\n },\n };\n}\n\nexport { createDeviceId };\n"],"mappings":";;;;;;AAAA,OAAO;AACP,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAQ/B,eAAsB,eAAuC;AAC3D,QAAM,QAAQ,MAAM,QAAQ;AAC5B,SAAO,MAAM,IAAI,gBAAgB,GAAG,SAAS;AAC/C;AAMA,eAAsB,gBAAgB,QAA4C;AAChF,MAAI,OAAQ,QAAO,OAAO;AAC1B,SAAQ,MAAM,aAAa,KAAM,eAAe;AAClD;AAEO,SAAS,eAAe,UAU7B;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACnC;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,14 @@
1
+ // src/version.ts
2
+ var SDK_VERSION = "1.0.1";
3
+
4
+ // src/constants.ts
5
+ var DEVICE_ID_COOKIE = "fewchurs_did";
6
+ var DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;
7
+ var SDK_HEADER = `next/${SDK_VERSION}`;
8
+
9
+ export {
10
+ DEVICE_ID_COOKIE,
11
+ DEVICE_ID_MAX_AGE,
12
+ SDK_HEADER
13
+ };
14
+ //# sourceMappingURL=chunk-W6QR6DEB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts","../src/constants.ts"],"sourcesContent":["// Generated by tools/stamp-version.mjs from @fewchurs/next's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import { SDK_VERSION } from \"./version\";\n\nexport const DEVICE_ID_COOKIE = \"fewchurs_did\";\nexport const DEVICE_ID_MAX_AGE = 60 * 60 * 24 * 365;\nexport { SDK_VERSION };\nexport const SDK_HEADER = `next/${SDK_VERSION}`;\n"],"mappings":";AACO,IAAM,cAAc;;;ACCpB,IAAM,mBAAmB;AACzB,IAAM,oBAAoB,KAAK,KAAK,KAAK;AAEzC,IAAM,aAAa,QAAQ,WAAW;","names":[]}
@@ -0,0 +1,28 @@
1
+ import { DeviceIdResolver } from './identity.mjs';
2
+ import '@fewchurs/react/core';
3
+
4
+ interface HandlerOptions {
5
+ apiKey?: string;
6
+ baseUrl?: string;
7
+ /** Where the handler is mounted, when it is not `/api/fewchurs`. */
8
+ apiPath?: string;
9
+ /** Return a stable, hashed id for the signed-in visitor so votes follow the account. */
10
+ deviceId?: DeviceIdResolver;
11
+ }
12
+ type RouteParams = {
13
+ route?: string[];
14
+ };
15
+ type RouteContext = {
16
+ params: Promise<RouteParams>;
17
+ };
18
+ type RouteHandler = (request: Request, context: RouteContext) => Promise<Response>;
19
+ declare function createHandler(options?: HandlerOptions): {
20
+ GET: RouteHandler;
21
+ POST: RouteHandler;
22
+ DELETE: RouteHandler;
23
+ };
24
+ declare const GET: RouteHandler;
25
+ declare const POST: RouteHandler;
26
+ declare const DELETE: RouteHandler;
27
+
28
+ export { DELETE, GET, type HandlerOptions, POST, type RouteHandler, createHandler };