@fewchurs/react 1.0.1 → 1.1.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.
- package/README.md +2 -0
- package/dist/core.d.mts +8 -3
- package/dist/core.d.ts +8 -3
- package/dist/core.js +8 -5
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +8 -5
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ import "@fewchurs/react/styles.css";
|
|
|
16
16
|
</FewchursProvider>;
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Add your domain to your app in the Fewchurs dashboard. The API key only works there.
|
|
20
|
+
|
|
19
21
|
`FewchursBoardDialog` renders the same board in a modal (`open`, `onClose`, plus every board
|
|
20
22
|
prop), and `initialFeatureId` opens the board on one request with its comments expanded.
|
|
21
23
|
|
package/dist/core.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type FewchursError = {
|
|
2
2
|
code: "invalid_key";
|
|
3
|
-
status: 401;
|
|
3
|
+
status: 401 | 403;
|
|
4
4
|
message: string;
|
|
5
5
|
} | {
|
|
6
6
|
code: "validation_failed";
|
|
@@ -108,6 +108,11 @@ interface FewchursClientOptions {
|
|
|
108
108
|
deviceId?: string | null | (() => string | null);
|
|
109
109
|
/** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
|
|
110
110
|
language?: string;
|
|
111
|
+
/**
|
|
112
|
+
* The domain the API key is checked against, for calls made from a server. A browser reports
|
|
113
|
+
* its own domain through `Origin`, which always wins, so leave this out in a SPA.
|
|
114
|
+
*/
|
|
115
|
+
appId?: string;
|
|
111
116
|
fetch?: FetchLike;
|
|
112
117
|
retries?: number;
|
|
113
118
|
retryDelayMs?: number;
|
|
@@ -138,9 +143,9 @@ interface FewchursClient {
|
|
|
138
143
|
}
|
|
139
144
|
declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
|
|
140
145
|
|
|
141
|
-
declare const SDK_VERSION = "1.0
|
|
146
|
+
declare const SDK_VERSION = "1.1.0";
|
|
142
147
|
|
|
143
|
-
declare const SDK_HEADER = "react/1.0
|
|
148
|
+
declare const SDK_HEADER = "react/1.1.0";
|
|
144
149
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
145
150
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
146
151
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
package/dist/core.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type FewchursError = {
|
|
2
2
|
code: "invalid_key";
|
|
3
|
-
status: 401;
|
|
3
|
+
status: 401 | 403;
|
|
4
4
|
message: string;
|
|
5
5
|
} | {
|
|
6
6
|
code: "validation_failed";
|
|
@@ -108,6 +108,11 @@ interface FewchursClientOptions {
|
|
|
108
108
|
deviceId?: string | null | (() => string | null);
|
|
109
109
|
/** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
|
|
110
110
|
language?: string;
|
|
111
|
+
/**
|
|
112
|
+
* The domain the API key is checked against, for calls made from a server. A browser reports
|
|
113
|
+
* its own domain through `Origin`, which always wins, so leave this out in a SPA.
|
|
114
|
+
*/
|
|
115
|
+
appId?: string;
|
|
111
116
|
fetch?: FetchLike;
|
|
112
117
|
retries?: number;
|
|
113
118
|
retryDelayMs?: number;
|
|
@@ -138,9 +143,9 @@ interface FewchursClient {
|
|
|
138
143
|
}
|
|
139
144
|
declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
|
|
140
145
|
|
|
141
|
-
declare const SDK_VERSION = "1.0
|
|
146
|
+
declare const SDK_VERSION = "1.1.0";
|
|
142
147
|
|
|
143
|
-
declare const SDK_HEADER = "react/1.0
|
|
148
|
+
declare const SDK_HEADER = "react/1.1.0";
|
|
144
149
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
145
150
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
146
151
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
package/dist/core.js
CHANGED
|
@@ -48,7 +48,7 @@ __export(core_exports, {
|
|
|
48
48
|
module.exports = __toCommonJS(core_exports);
|
|
49
49
|
|
|
50
50
|
// src/client/version.ts
|
|
51
|
-
var SDK_VERSION = "1.0
|
|
51
|
+
var SDK_VERSION = "1.1.0";
|
|
52
52
|
|
|
53
53
|
// src/client/constants.ts
|
|
54
54
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -91,6 +91,8 @@ function errorFromResponse(status, body) {
|
|
|
91
91
|
switch (code) {
|
|
92
92
|
case "invalid_key":
|
|
93
93
|
return { code: "invalid_key", status: 401, message };
|
|
94
|
+
case "wrong_app":
|
|
95
|
+
return { code: "invalid_key", status: 403, message };
|
|
94
96
|
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
95
97
|
// so it collapses into validation_failed rather than widening the public union.
|
|
96
98
|
case "missing_device_id":
|
|
@@ -240,6 +242,7 @@ function createFewchursClient(options = {}) {
|
|
|
240
242
|
const deviceId = getDeviceId();
|
|
241
243
|
if (deviceId) result["X-Device-Id"] = deviceId;
|
|
242
244
|
if (options.language) result["Accept-Language"] = options.language;
|
|
245
|
+
if (options.appId) result["X-Fewchurs-App-Id"] = options.appId;
|
|
243
246
|
return result;
|
|
244
247
|
}
|
|
245
248
|
function url(path, query) {
|
|
@@ -278,7 +281,7 @@ function createFewchursClient(options = {}) {
|
|
|
278
281
|
continue;
|
|
279
282
|
}
|
|
280
283
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
281
|
-
warnAboutKey(failure
|
|
284
|
+
warnAboutKey(failure);
|
|
282
285
|
return { ok: false, error: failure };
|
|
283
286
|
}
|
|
284
287
|
return { ok: false, error: networkError() };
|
|
@@ -370,12 +373,12 @@ function messageOf(error) {
|
|
|
370
373
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
371
374
|
}
|
|
372
375
|
var warnedAboutKey = false;
|
|
373
|
-
function warnAboutKey(
|
|
374
|
-
if (code !== "invalid_key" || warnedAboutKey) return;
|
|
376
|
+
function warnAboutKey(error) {
|
|
377
|
+
if (error.code !== "invalid_key" || warnedAboutKey) return;
|
|
375
378
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
376
379
|
warnedAboutKey = true;
|
|
377
380
|
console.error(
|
|
378
|
-
"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
381
|
+
error.status === 403 ? `[fewchurs] ${error.message}` : "[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
379
382
|
);
|
|
380
383
|
}
|
|
381
384
|
|
package/dist/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts","../src/client/version.ts","../src/client/constants.ts","../src/client/errors.ts","../src/client/normalize.ts","../src/client/validate.ts","../src/client/client.ts","../src/identity/deviceId.ts","../src/store/keys.ts"],"sourcesContent":["export { createFewchursClient } from \"./client/client\";\nexport type { FetchLike, FewchursClient, FewchursClientOptions } from \"./client/client\";\nexport {\n DEFAULT_BASE_URL,\n DEFAULT_PAGE_SIZE,\n LIMITS,\n ROADMAP_STATUSES,\n SDK_HEADER,\n SDK_VERSION,\n} from \"./client/constants\";\nexport { createDeviceId } from \"./identity/deviceId\";\nexport { featureKey, listKey } from \"./store/keys\";\nexport type { ListQuery } from \"./store/keys\";\n// Types only: a server component describes seeded state without importing client runtime.\nexport type {\n CommentsState,\n FewchursEvent,\n FewchursEventName,\n ListState,\n LoadStatus,\n StoreState,\n} from \"./store/store\";\nexport { errorFromResponse, networkError, userMessage, validationError } from \"./client/errors\";\nexport type { FewchursError, Result } from \"./client/errors\";\nexport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeStatus,\n normalizeVote,\n} from \"./client/normalize\";\nexport { validateCommentInput, validateEmail, validateFeatureInput } from \"./client/validate\";\nexport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FeatureSort,\n FeatureStatus,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"./types\";\n","// Generated by tools/stamp-version.mjs from @fewchurs/react's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import type { FeatureStatus } from \"../types\";\nimport { SDK_VERSION } from \"./version\";\n\nexport { SDK_VERSION };\nexport const SDK_HEADER = `react/${SDK_VERSION}`;\nexport const DEFAULT_BASE_URL = \"https://www.fewchurs.com/api/v1\";\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */\nexport const LIMITS = {\n titleMin: 3,\n titleMax: 100,\n descriptionMax: 1000,\n commentMin: 1,\n commentMax: 1000,\n authorNameMax: 60,\n} as const;\n\n/** Deliberately loose: the server owns the real check, this only catches obvious typos. */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */\nexport const ROADMAP_STATUSES: FeatureStatus[] = [\"planned\", \"in_progress\", \"done\"];\n","export type FewchursError =\n | { code: \"invalid_key\"; status: 401; message: string }\n | { code: \"validation_failed\"; status: 400; message: string }\n | { code: \"rate_limited\"; status: 429; message: string }\n | { code: \"not_found\"; status: 404; message: string }\n | { code: \"limit_reached\"; status: 402; message: string }\n | { code: \"server\"; status: number; message: string }\n | { code: \"network\"; message: string };\n\nexport type Result<T> = { ok: true; data: T } | { ok: false; error: FewchursError };\n\ninterface ServerErrorBody {\n error?: { code?: string; message?: string };\n}\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Feedback is unavailable right now.\",\n validation_failed: \"Please check what you entered and try again.\",\n rate_limited: \"You have reached today's limit. Try again tomorrow.\",\n not_found: \"That request no longer exists.\",\n limit_reached: \"Feedback is unavailable right now.\",\n server: \"Something went wrong. Please try again.\",\n network: \"Could not reach the server. Check your connection.\",\n};\n\n/** Safe to show a visitor: never leaks a server message that may name internals. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n return USER_MESSAGES[error.code];\n}\n\nexport function networkError(message = \"Network request failed.\"): FewchursError {\n return { code: \"network\", message };\n}\n\nexport function validationError(message: string): FewchursError {\n return { code: \"validation_failed\", status: 400, message };\n}\n\nexport function errorFromResponse(status: number, body: unknown): FewchursError {\n const code = (body as ServerErrorBody | null)?.error?.code;\n const message = (body as ServerErrorBody | null)?.error?.message ?? `Request failed (${status}).`;\n\n switch (code) {\n case \"invalid_key\":\n return { code: \"invalid_key\", status: 401, message };\n // missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,\n // so it collapses into validation_failed rather than widening the public union.\n case \"missing_device_id\":\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message };\n case \"rate_limited\":\n return { code: \"rate_limited\", status: 429, message };\n case \"not_found\":\n return { code: \"not_found\", status: 404, message };\n case \"limit_reached\":\n return { code: \"limit_reached\", status: 402, message };\n default:\n return status === 401\n ? { code: \"invalid_key\", status: 401, message }\n : { code: \"server\", status, message };\n }\n}\n","import type {\n Comment,\n CommentPage,\n Feature,\n FeaturePage,\n FeatureStatus,\n FewchursConfig,\n} from \"../types\";\n\nconst KNOWN_STATUSES = new Set<string>([\"open\", \"planned\", \"in_progress\", \"done\", \"declined\"]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : {};\n}\n\nfunction str(value: unknown, fallback = \"\"): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\nfunction bool(value: unknown): boolean {\n return value === true;\n}\n\nexport function normalizeStatus(value: unknown): FeatureStatus {\n return typeof value === \"string\" && KNOWN_STATUSES.has(value)\n ? (value as FeatureStatus)\n : \"unknown\";\n}\n\n// `isMine` is the server's answer once it ships; until then the raw author device id is the\n// only signal, so it is compared here and never surfaced on the public type.\nfunction isMine(raw: Record<string, unknown>, ownerKey: string, deviceId: string | null): boolean {\n if (typeof raw.isMine === \"boolean\") return raw.isMine;\n const owner = raw[ownerKey];\n return typeof owner === \"string\" && owner.length > 0 && owner === deviceId;\n}\n\nexport function normalizeFeature(value: unknown, deviceId: string | null): Feature {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n title: str(raw.title),\n description: str(raw.description),\n status: normalizeStatus(raw.status),\n upvoteCount: num(raw.upvoteCount),\n commentCount: num(raw.commentCount),\n followerCount: num(raw.followerCount),\n hasVoted: bool(raw.hasVoted),\n isFollowing: bool(raw.isFollowing),\n isMine: isMine(raw, \"authorDeviceId\", deviceId),\n createdAt: str(raw.createdAt),\n updatedAt: str(raw.updatedAt, str(raw.createdAt)),\n };\n}\n\nexport function normalizeComment(value: unknown, deviceId: string | null): Comment {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n text: str(raw.text),\n authorName: str(raw.authorName, \"App User\"),\n isDeveloper: bool(raw.isDeveloper),\n isMine: isMine(raw, \"deviceId\", deviceId),\n createdAt: str(raw.createdAt),\n };\n}\n\nexport function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage {\n const raw = asRecord(value);\n const features = Array.isArray(raw.features) ? raw.features : [];\n return {\n features: features.map((feature) => normalizeFeature(feature, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage {\n const raw = asRecord(value);\n const comments = Array.isArray(raw.comments) ? raw.comments : [];\n return {\n comments: comments.map((comment) => normalizeComment(comment, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeConfig(value: unknown): FewchursConfig {\n const raw = asRecord(value);\n return {\n showBranding: raw.showBranding !== false,\n hideVoteCounts: bool(raw.hideVoteCounts),\n appName: typeof raw.appName === \"string\" && raw.appName ? raw.appName : null,\n };\n}\n\nexport function normalizeVote(value: unknown): { upvoteCount: number; hasVoted: boolean } {\n const raw = asRecord(value);\n return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };\n}\n","import { EMAIL_PATTERN, LIMITS } from \"./constants\";\nimport { validationError, type FewchursError } from \"./errors\";\nimport type { CreateCommentInput, SubmitFeatureInput } from \"../types\";\n\nexport function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null {\n const title = input.title.trim();\n if (title.length < LIMITS.titleMin) {\n return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);\n }\n if (title.length > LIMITS.titleMax) {\n return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);\n }\n if ((input.description ?? \"\").trim().length > LIMITS.descriptionMax) {\n return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);\n }\n if (input.email !== undefined && input.email !== \"\" && !isEmail(input.email)) {\n return validationError(\"Enter a valid email address.\");\n }\n return null;\n}\n\nexport function validateCommentInput(input: CreateCommentInput): FewchursError | null {\n const text = input.text.trim();\n if (text.length < LIMITS.commentMin) return validationError(\"Write a comment first.\");\n if (text.length > LIMITS.commentMax) {\n return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);\n }\n if ((input.authorName ?? \"\").trim().length > LIMITS.authorNameMax) {\n return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);\n }\n return null;\n}\n\nexport function validateEmail(email: string): FewchursError | null {\n return isEmail(email) ? null : validationError(\"Enter a valid email address.\");\n}\n\nfunction isEmail(email: string): boolean {\n return EMAIL_PATTERN.test(email.trim());\n}\n","import { DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, SDK_HEADER } from \"./constants\";\nimport { errorFromResponse, networkError, type Result } from \"./errors\";\nimport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeVote,\n} from \"./normalize\";\nimport { validateCommentInput, validateEmail, validateFeatureInput } from \"./validate\";\nimport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"../types\";\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface FewchursClientOptions {\n /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */\n apiKey?: string;\n /** `https://www.fewchurs.com/api/v1` by default, or a proxy path such as `/api/fewchurs`. */\n baseUrl?: string;\n deviceId?: string | null | (() => string | null);\n /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */\n language?: string;\n fetch?: FetchLike;\n retries?: number;\n retryDelayMs?: number;\n}\n\nexport interface FewchursClient {\n readonly baseUrl: string;\n getDeviceId(): string | null;\n getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;\n listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;\n getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;\n submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;\n vote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n unvote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n follow(featureId: string, email: string): Promise<Result<{ following: boolean }>>;\n unfollow(featureId: string): Promise<Result<{ following: boolean }>>;\n listComments(\n featureId: string,\n params?: ListCommentsParams,\n signal?: AbortSignal,\n ): Promise<Result<CommentPage>>;\n createComment(input: CreateCommentInput): Promise<Result<Comment>>;\n}\n\ninterface RequestOptions {\n method?: \"GET\" | \"POST\" | \"DELETE\";\n body?: unknown;\n query?: Record<string, string | number | undefined | null>;\n signal?: AbortSignal;\n}\n\nexport function createFewchursClient(options: FewchursClientOptions = {}): FewchursClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);\n const retries = options.retries ?? 1;\n const retryDelayMs = options.retryDelayMs ?? 250;\n\n function getDeviceId(): string | null {\n return typeof options.deviceId === \"function\" ? options.deviceId() : (options.deviceId ?? null);\n }\n\n function headers(hasBody: boolean): Record<string, string> {\n const result: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Fewchurs-Sdk\": SDK_HEADER,\n };\n if (hasBody) result[\"Content-Type\"] = \"application/json\";\n if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;\n const deviceId = getDeviceId();\n if (deviceId) result[\"X-Device-Id\"] = deviceId;\n if (options.language) result[\"Accept-Language\"] = options.language;\n return result;\n }\n\n function url(path: string, query?: RequestOptions[\"query\"]): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== null && value !== \"\") search.set(key, String(value));\n }\n const qs = search.toString();\n return `${baseUrl}${path}${qs ? `?${qs}` : \"\"}`;\n }\n\n async function request<T>(\n path: string,\n map: (body: unknown) => T,\n { method = \"GET\", body, query, signal }: RequestOptions = {},\n ): Promise<Result<T>> {\n if (!doFetch) return { ok: false, error: networkError(\"No fetch implementation available.\") };\n\n const target = url(path, query);\n const init: RequestInit = { method, headers: headers(body !== undefined) };\n if (body !== undefined) init.body = JSON.stringify(body);\n if (signal) init.signal = signal;\n\n // Only GETs are retried: a retried POST could double-submit a request or a comment.\n const attempts = method === \"GET\" ? retries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n let response: Response;\n try {\n response = await doFetch(target, init);\n } catch (error) {\n if (signal?.aborted) return { ok: false, error: networkError(\"Request aborted.\") };\n if (attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n return { ok: false, error: networkError(messageOf(error)) };\n }\n\n if (response.ok) {\n const payload = await readJson(response);\n return { ok: true, data: map(payload) };\n }\n\n if (response.status >= 500 && attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n\n const failure = errorFromResponse(response.status, await readJson(response));\n warnAboutKey(failure.code);\n return { ok: false, error: failure };\n }\n\n return { ok: false, error: networkError() };\n }\n\n return {\n baseUrl,\n getDeviceId,\n\n getConfig: (signal) => request(\"/config\", normalizeConfig, { signal }),\n\n listFeatures: (params = {}, signal) =>\n request(\"/features\", (body) => normalizeFeaturePage(body, getDeviceId()), {\n query: {\n sort: params.sort ?? \"new\",\n limit: params.limit ?? DEFAULT_PAGE_SIZE,\n cursor: params.cursor ?? undefined,\n status: params.status?.length ? params.status.join(\",\") : undefined,\n },\n signal,\n }),\n\n getFeature: (featureId, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}`,\n (body) => normalizeFeature(body, getDeviceId()),\n { signal },\n ),\n\n submitFeature: async (input) => {\n const invalid = validateFeatureInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/features\", (body) => normalizeFeature(body, getDeviceId()), {\n method: \"POST\",\n body: {\n title: input.title.trim(),\n description: (input.description ?? \"\").trim(),\n ...(input.email ? { email: input.email.trim() } : {}),\n isSubscriber: input.isSubscriber ?? false,\n },\n });\n },\n\n vote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: \"POST\" }),\n\n unvote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {\n method: \"DELETE\",\n }),\n\n follow: async (featureId, email) => {\n const invalid = validateEmail(email);\n if (invalid) return { ok: false, error: invalid };\n return request(\n `/features/${encodeURIComponent(featureId)}/follow`,\n () => ({ following: true }),\n {\n method: \"POST\",\n body: { email: email.trim() },\n },\n );\n },\n\n unfollow: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {\n method: \"DELETE\",\n }),\n\n listComments: (featureId, params = {}, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}/comments`,\n (body) => normalizeCommentPage(body, getDeviceId()),\n {\n query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? undefined },\n signal,\n },\n ),\n\n // The server takes the feature id in the body here; POST /features/{id}/comments is a 405.\n createComment: async (input) => {\n const invalid = validateCommentInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/comments\", (body) => normalizeComment(body, getDeviceId()), {\n method: \"POST\",\n body: {\n featureId: input.featureId,\n text: input.text.trim(),\n ...(input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}),\n },\n });\n },\n };\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : \"Network request failed.\";\n}\n\n// Declared, not imported from @types/node: this is a browser package, and bundlers replace\n// `process.env.NODE_ENV` inline, so the check costs nothing in a production build.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet warnedAboutKey = false;\n\nfunction warnAboutKey(code: string): void {\n if (code !== \"invalid_key\" || warnedAboutKey) return;\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n warnedAboutKey = true;\n console.error(\n \"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard.\",\n );\n}\n","const COOKIE_NAME = \"fewchurs_did\";\nconst STORAGE_KEY = \"fewchurs_device_id\";\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nexport function createDeviceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport function readDeviceId(): string | null {\n if (typeof document === \"undefined\") return null;\n return readCookie() ?? readStorage();\n}\n\n/**\n * Cookie first, storage second: a cookie is the only copy a same-origin server can read, so\n * the board renders with the same identity on both sides and never mismatches on hydration.\n */\nexport function resolveDeviceId(): string {\n const existing = readDeviceId();\n if (existing) {\n persist(existing);\n return existing;\n }\n const created = createDeviceId();\n persist(created);\n return created;\n}\n\nfunction persist(deviceId: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${COOKIE_NAME}=${encodeURIComponent(deviceId)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;\n } catch {\n // Cookies can be blocked entirely; storage below is the fallback.\n }\n try {\n localStorage.setItem(STORAGE_KEY, deviceId);\n } catch {\n // Private-mode storage throws on write; the cookie or a fresh id per visit still works.\n }\n}\n\nfunction readCookie(): string | null {\n const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction readStorage(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nexport { COOKIE_NAME as DEVICE_ID_COOKIE };\n","import type { FeatureStatus, FeatureSort } from \"../types\";\n\nexport interface ListQuery {\n sort: FeatureSort;\n status?: FeatureStatus[];\n}\n\nexport function listKey({ sort, status }: ListQuery): string {\n return status?.length ? `${sort}:${[...status].sort().join(\",\")}` : sort;\n}\n\n/**\n * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked\n * request goes through the same vote, comment and follow updates as a row in a list.\n */\nexport function featureKey(featureId: string): string {\n return `feature:${featureId}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/client/version.ts","../src/client/constants.ts","../src/client/errors.ts","../src/client/normalize.ts","../src/client/validate.ts","../src/client/client.ts","../src/identity/deviceId.ts","../src/store/keys.ts"],"sourcesContent":["export { createFewchursClient } from \"./client/client\";\nexport type { FetchLike, FewchursClient, FewchursClientOptions } from \"./client/client\";\nexport {\n DEFAULT_BASE_URL,\n DEFAULT_PAGE_SIZE,\n LIMITS,\n ROADMAP_STATUSES,\n SDK_HEADER,\n SDK_VERSION,\n} from \"./client/constants\";\nexport { createDeviceId } from \"./identity/deviceId\";\nexport { featureKey, listKey } from \"./store/keys\";\nexport type { ListQuery } from \"./store/keys\";\n// Types only: a server component describes seeded state without importing client runtime.\nexport type {\n CommentsState,\n FewchursEvent,\n FewchursEventName,\n ListState,\n LoadStatus,\n StoreState,\n} from \"./store/store\";\nexport { errorFromResponse, networkError, userMessage, validationError } from \"./client/errors\";\nexport type { FewchursError, Result } from \"./client/errors\";\nexport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeStatus,\n normalizeVote,\n} from \"./client/normalize\";\nexport { validateCommentInput, validateEmail, validateFeatureInput } from \"./client/validate\";\nexport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FeatureSort,\n FeatureStatus,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"./types\";\n","// Generated by tools/stamp-version.mjs from @fewchurs/react's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.1.0\";\n","import type { FeatureStatus } from \"../types\";\nimport { SDK_VERSION } from \"./version\";\n\nexport { SDK_VERSION };\nexport const SDK_HEADER = `react/${SDK_VERSION}`;\nexport const DEFAULT_BASE_URL = \"https://www.fewchurs.com/api/v1\";\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */\nexport const LIMITS = {\n titleMin: 3,\n titleMax: 100,\n descriptionMax: 1000,\n commentMin: 1,\n commentMax: 1000,\n authorNameMax: 60,\n} as const;\n\n/** Deliberately loose: the server owns the real check, this only catches obvious typos. */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */\nexport const ROADMAP_STATUSES: FeatureStatus[] = [\"planned\", \"in_progress\", \"done\"];\n","export type FewchursError =\n | { code: \"invalid_key\"; status: 401 | 403; message: string }\n | { code: \"validation_failed\"; status: 400; message: string }\n | { code: \"rate_limited\"; status: 429; message: string }\n | { code: \"not_found\"; status: 404; message: string }\n | { code: \"limit_reached\"; status: 402; message: string }\n | { code: \"server\"; status: number; message: string }\n | { code: \"network\"; message: string };\n\nexport type Result<T> = { ok: true; data: T } | { ok: false; error: FewchursError };\n\ninterface ServerErrorBody {\n error?: { code?: string; message?: string };\n}\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Feedback is unavailable right now.\",\n validation_failed: \"Please check what you entered and try again.\",\n rate_limited: \"You have reached today's limit. Try again tomorrow.\",\n not_found: \"That request no longer exists.\",\n limit_reached: \"Feedback is unavailable right now.\",\n server: \"Something went wrong. Please try again.\",\n network: \"Could not reach the server. Check your connection.\",\n};\n\n/** Safe to show a visitor: never leaks a server message that may name internals. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n return USER_MESSAGES[error.code];\n}\n\nexport function networkError(message = \"Network request failed.\"): FewchursError {\n return { code: \"network\", message };\n}\n\nexport function validationError(message: string): FewchursError {\n return { code: \"validation_failed\", status: 400, message };\n}\n\nexport function errorFromResponse(status: number, body: unknown): FewchursError {\n const code = (body as ServerErrorBody | null)?.error?.code;\n const message = (body as ServerErrorBody | null)?.error?.message ?? `Request failed (${status}).`;\n\n switch (code) {\n case \"invalid_key\":\n return { code: \"invalid_key\", status: 401, message };\n case \"wrong_app\":\n return { code: \"invalid_key\", status: 403, message };\n // missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,\n // so it collapses into validation_failed rather than widening the public union.\n case \"missing_device_id\":\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message };\n case \"rate_limited\":\n return { code: \"rate_limited\", status: 429, message };\n case \"not_found\":\n return { code: \"not_found\", status: 404, message };\n case \"limit_reached\":\n return { code: \"limit_reached\", status: 402, message };\n default:\n return status === 401\n ? { code: \"invalid_key\", status: 401, message }\n : { code: \"server\", status, message };\n }\n}\n","import type {\n Comment,\n CommentPage,\n Feature,\n FeaturePage,\n FeatureStatus,\n FewchursConfig,\n} from \"../types\";\n\nconst KNOWN_STATUSES = new Set<string>([\"open\", \"planned\", \"in_progress\", \"done\", \"declined\"]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : {};\n}\n\nfunction str(value: unknown, fallback = \"\"): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\nfunction bool(value: unknown): boolean {\n return value === true;\n}\n\nexport function normalizeStatus(value: unknown): FeatureStatus {\n return typeof value === \"string\" && KNOWN_STATUSES.has(value)\n ? (value as FeatureStatus)\n : \"unknown\";\n}\n\n// `isMine` is the server's answer once it ships; until then the raw author device id is the\n// only signal, so it is compared here and never surfaced on the public type.\nfunction isMine(raw: Record<string, unknown>, ownerKey: string, deviceId: string | null): boolean {\n if (typeof raw.isMine === \"boolean\") return raw.isMine;\n const owner = raw[ownerKey];\n return typeof owner === \"string\" && owner.length > 0 && owner === deviceId;\n}\n\nexport function normalizeFeature(value: unknown, deviceId: string | null): Feature {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n title: str(raw.title),\n description: str(raw.description),\n status: normalizeStatus(raw.status),\n upvoteCount: num(raw.upvoteCount),\n commentCount: num(raw.commentCount),\n followerCount: num(raw.followerCount),\n hasVoted: bool(raw.hasVoted),\n isFollowing: bool(raw.isFollowing),\n isMine: isMine(raw, \"authorDeviceId\", deviceId),\n createdAt: str(raw.createdAt),\n updatedAt: str(raw.updatedAt, str(raw.createdAt)),\n };\n}\n\nexport function normalizeComment(value: unknown, deviceId: string | null): Comment {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n text: str(raw.text),\n authorName: str(raw.authorName, \"App User\"),\n isDeveloper: bool(raw.isDeveloper),\n isMine: isMine(raw, \"deviceId\", deviceId),\n createdAt: str(raw.createdAt),\n };\n}\n\nexport function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage {\n const raw = asRecord(value);\n const features = Array.isArray(raw.features) ? raw.features : [];\n return {\n features: features.map((feature) => normalizeFeature(feature, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage {\n const raw = asRecord(value);\n const comments = Array.isArray(raw.comments) ? raw.comments : [];\n return {\n comments: comments.map((comment) => normalizeComment(comment, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeConfig(value: unknown): FewchursConfig {\n const raw = asRecord(value);\n return {\n showBranding: raw.showBranding !== false,\n hideVoteCounts: bool(raw.hideVoteCounts),\n appName: typeof raw.appName === \"string\" && raw.appName ? raw.appName : null,\n };\n}\n\nexport function normalizeVote(value: unknown): { upvoteCount: number; hasVoted: boolean } {\n const raw = asRecord(value);\n return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };\n}\n","import { EMAIL_PATTERN, LIMITS } from \"./constants\";\nimport { validationError, type FewchursError } from \"./errors\";\nimport type { CreateCommentInput, SubmitFeatureInput } from \"../types\";\n\nexport function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null {\n const title = input.title.trim();\n if (title.length < LIMITS.titleMin) {\n return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);\n }\n if (title.length > LIMITS.titleMax) {\n return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);\n }\n if ((input.description ?? \"\").trim().length > LIMITS.descriptionMax) {\n return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);\n }\n if (input.email !== undefined && input.email !== \"\" && !isEmail(input.email)) {\n return validationError(\"Enter a valid email address.\");\n }\n return null;\n}\n\nexport function validateCommentInput(input: CreateCommentInput): FewchursError | null {\n const text = input.text.trim();\n if (text.length < LIMITS.commentMin) return validationError(\"Write a comment first.\");\n if (text.length > LIMITS.commentMax) {\n return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);\n }\n if ((input.authorName ?? \"\").trim().length > LIMITS.authorNameMax) {\n return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);\n }\n return null;\n}\n\nexport function validateEmail(email: string): FewchursError | null {\n return isEmail(email) ? null : validationError(\"Enter a valid email address.\");\n}\n\nfunction isEmail(email: string): boolean {\n return EMAIL_PATTERN.test(email.trim());\n}\n","import { DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, SDK_HEADER } from \"./constants\";\nimport { errorFromResponse, networkError, type FewchursError, type Result } from \"./errors\";\nimport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeVote,\n} from \"./normalize\";\nimport { validateCommentInput, validateEmail, validateFeatureInput } from \"./validate\";\nimport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"../types\";\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface FewchursClientOptions {\n /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */\n apiKey?: string;\n /** `https://www.fewchurs.com/api/v1` by default, or a proxy path such as `/api/fewchurs`. */\n baseUrl?: string;\n deviceId?: string | null | (() => string | null);\n /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */\n language?: string;\n /**\n * The domain the API key is checked against, for calls made from a server. A browser reports\n * its own domain through `Origin`, which always wins, so leave this out in a SPA.\n */\n appId?: string;\n fetch?: FetchLike;\n retries?: number;\n retryDelayMs?: number;\n}\n\nexport interface FewchursClient {\n readonly baseUrl: string;\n getDeviceId(): string | null;\n getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;\n listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;\n getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;\n submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;\n vote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n unvote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n follow(featureId: string, email: string): Promise<Result<{ following: boolean }>>;\n unfollow(featureId: string): Promise<Result<{ following: boolean }>>;\n listComments(\n featureId: string,\n params?: ListCommentsParams,\n signal?: AbortSignal,\n ): Promise<Result<CommentPage>>;\n createComment(input: CreateCommentInput): Promise<Result<Comment>>;\n}\n\ninterface RequestOptions {\n method?: \"GET\" | \"POST\" | \"DELETE\";\n body?: unknown;\n query?: Record<string, string | number | undefined | null>;\n signal?: AbortSignal;\n}\n\nexport function createFewchursClient(options: FewchursClientOptions = {}): FewchursClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);\n const retries = options.retries ?? 1;\n const retryDelayMs = options.retryDelayMs ?? 250;\n\n function getDeviceId(): string | null {\n return typeof options.deviceId === \"function\" ? options.deviceId() : (options.deviceId ?? null);\n }\n\n function headers(hasBody: boolean): Record<string, string> {\n const result: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Fewchurs-Sdk\": SDK_HEADER,\n };\n if (hasBody) result[\"Content-Type\"] = \"application/json\";\n if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;\n const deviceId = getDeviceId();\n if (deviceId) result[\"X-Device-Id\"] = deviceId;\n if (options.language) result[\"Accept-Language\"] = options.language;\n if (options.appId) result[\"X-Fewchurs-App-Id\"] = options.appId;\n return result;\n }\n\n function url(path: string, query?: RequestOptions[\"query\"]): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== null && value !== \"\") search.set(key, String(value));\n }\n const qs = search.toString();\n return `${baseUrl}${path}${qs ? `?${qs}` : \"\"}`;\n }\n\n async function request<T>(\n path: string,\n map: (body: unknown) => T,\n { method = \"GET\", body, query, signal }: RequestOptions = {},\n ): Promise<Result<T>> {\n if (!doFetch) return { ok: false, error: networkError(\"No fetch implementation available.\") };\n\n const target = url(path, query);\n const init: RequestInit = { method, headers: headers(body !== undefined) };\n if (body !== undefined) init.body = JSON.stringify(body);\n if (signal) init.signal = signal;\n\n // Only GETs are retried: a retried POST could double-submit a request or a comment.\n const attempts = method === \"GET\" ? retries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n let response: Response;\n try {\n response = await doFetch(target, init);\n } catch (error) {\n if (signal?.aborted) return { ok: false, error: networkError(\"Request aborted.\") };\n if (attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n return { ok: false, error: networkError(messageOf(error)) };\n }\n\n if (response.ok) {\n const payload = await readJson(response);\n return { ok: true, data: map(payload) };\n }\n\n if (response.status >= 500 && attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n\n const failure = errorFromResponse(response.status, await readJson(response));\n warnAboutKey(failure);\n return { ok: false, error: failure };\n }\n\n return { ok: false, error: networkError() };\n }\n\n return {\n baseUrl,\n getDeviceId,\n\n getConfig: (signal) => request(\"/config\", normalizeConfig, { signal }),\n\n listFeatures: (params = {}, signal) =>\n request(\"/features\", (body) => normalizeFeaturePage(body, getDeviceId()), {\n query: {\n sort: params.sort ?? \"new\",\n limit: params.limit ?? DEFAULT_PAGE_SIZE,\n cursor: params.cursor ?? undefined,\n status: params.status?.length ? params.status.join(\",\") : undefined,\n },\n signal,\n }),\n\n getFeature: (featureId, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}`,\n (body) => normalizeFeature(body, getDeviceId()),\n { signal },\n ),\n\n submitFeature: async (input) => {\n const invalid = validateFeatureInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/features\", (body) => normalizeFeature(body, getDeviceId()), {\n method: \"POST\",\n body: {\n title: input.title.trim(),\n description: (input.description ?? \"\").trim(),\n ...(input.email ? { email: input.email.trim() } : {}),\n isSubscriber: input.isSubscriber ?? false,\n },\n });\n },\n\n vote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: \"POST\" }),\n\n unvote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {\n method: \"DELETE\",\n }),\n\n follow: async (featureId, email) => {\n const invalid = validateEmail(email);\n if (invalid) return { ok: false, error: invalid };\n return request(\n `/features/${encodeURIComponent(featureId)}/follow`,\n () => ({ following: true }),\n {\n method: \"POST\",\n body: { email: email.trim() },\n },\n );\n },\n\n unfollow: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {\n method: \"DELETE\",\n }),\n\n listComments: (featureId, params = {}, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}/comments`,\n (body) => normalizeCommentPage(body, getDeviceId()),\n {\n query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? undefined },\n signal,\n },\n ),\n\n // The server takes the feature id in the body here; POST /features/{id}/comments is a 405.\n createComment: async (input) => {\n const invalid = validateCommentInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/comments\", (body) => normalizeComment(body, getDeviceId()), {\n method: \"POST\",\n body: {\n featureId: input.featureId,\n text: input.text.trim(),\n ...(input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}),\n },\n });\n },\n };\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : \"Network request failed.\";\n}\n\n// Declared, not imported from @types/node: this is a browser package, and bundlers replace\n// `process.env.NODE_ENV` inline, so the check costs nothing in a production build.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet warnedAboutKey = false;\n\nfunction warnAboutKey(error: FewchursError): void {\n if (error.code !== \"invalid_key\" || warnedAboutKey) return;\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n warnedAboutKey = true;\n console.error(\n error.status === 403\n ? `[fewchurs] ${error.message}`\n : \"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard.\",\n );\n}\n","const COOKIE_NAME = \"fewchurs_did\";\nconst STORAGE_KEY = \"fewchurs_device_id\";\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nexport function createDeviceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport function readDeviceId(): string | null {\n if (typeof document === \"undefined\") return null;\n return readCookie() ?? readStorage();\n}\n\n/**\n * Cookie first, storage second: a cookie is the only copy a same-origin server can read, so\n * the board renders with the same identity on both sides and never mismatches on hydration.\n */\nexport function resolveDeviceId(): string {\n const existing = readDeviceId();\n if (existing) {\n persist(existing);\n return existing;\n }\n const created = createDeviceId();\n persist(created);\n return created;\n}\n\nfunction persist(deviceId: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${COOKIE_NAME}=${encodeURIComponent(deviceId)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;\n } catch {\n // Cookies can be blocked entirely; storage below is the fallback.\n }\n try {\n localStorage.setItem(STORAGE_KEY, deviceId);\n } catch {\n // Private-mode storage throws on write; the cookie or a fresh id per visit still works.\n }\n}\n\nfunction readCookie(): string | null {\n const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction readStorage(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nexport { COOKIE_NAME as DEVICE_ID_COOKIE };\n","import type { FeatureStatus, FeatureSort } from \"../types\";\n\nexport interface ListQuery {\n sort: FeatureSort;\n status?: FeatureStatus[];\n}\n\nexport function listKey({ sort, status }: ListQuery): string {\n return status?.length ? `${sort}:${[...status].sort().join(\",\")}` : sort;\n}\n\n/**\n * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked\n * request goes through the same vote, comment and follow updates as a row in a list.\n */\nexport function featureKey(featureId: string): string {\n return `feature:${featureId}`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/core.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client/version.ts
|
|
2
|
-
var SDK_VERSION = "1.0
|
|
2
|
+
var SDK_VERSION = "1.1.0";
|
|
3
3
|
|
|
4
4
|
// src/client/constants.ts
|
|
5
5
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -42,6 +42,8 @@ function errorFromResponse(status, body) {
|
|
|
42
42
|
switch (code) {
|
|
43
43
|
case "invalid_key":
|
|
44
44
|
return { code: "invalid_key", status: 401, message };
|
|
45
|
+
case "wrong_app":
|
|
46
|
+
return { code: "invalid_key", status: 403, message };
|
|
45
47
|
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
46
48
|
// so it collapses into validation_failed rather than widening the public union.
|
|
47
49
|
case "missing_device_id":
|
|
@@ -191,6 +193,7 @@ function createFewchursClient(options = {}) {
|
|
|
191
193
|
const deviceId = getDeviceId();
|
|
192
194
|
if (deviceId) result["X-Device-Id"] = deviceId;
|
|
193
195
|
if (options.language) result["Accept-Language"] = options.language;
|
|
196
|
+
if (options.appId) result["X-Fewchurs-App-Id"] = options.appId;
|
|
194
197
|
return result;
|
|
195
198
|
}
|
|
196
199
|
function url(path, query) {
|
|
@@ -229,7 +232,7 @@ function createFewchursClient(options = {}) {
|
|
|
229
232
|
continue;
|
|
230
233
|
}
|
|
231
234
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
232
|
-
warnAboutKey(failure
|
|
235
|
+
warnAboutKey(failure);
|
|
233
236
|
return { ok: false, error: failure };
|
|
234
237
|
}
|
|
235
238
|
return { ok: false, error: networkError() };
|
|
@@ -321,12 +324,12 @@ function messageOf(error) {
|
|
|
321
324
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
322
325
|
}
|
|
323
326
|
var warnedAboutKey = false;
|
|
324
|
-
function warnAboutKey(
|
|
325
|
-
if (code !== "invalid_key" || warnedAboutKey) return;
|
|
327
|
+
function warnAboutKey(error) {
|
|
328
|
+
if (error.code !== "invalid_key" || warnedAboutKey) return;
|
|
326
329
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
327
330
|
warnedAboutKey = true;
|
|
328
331
|
console.error(
|
|
329
|
-
"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
332
|
+
error.status === 403 ? `[fewchurs] ${error.message}` : "[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
330
333
|
);
|
|
331
334
|
}
|
|
332
335
|
|
package/dist/core.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/version.ts","../src/client/constants.ts","../src/client/errors.ts","../src/client/normalize.ts","../src/client/validate.ts","../src/client/client.ts","../src/identity/deviceId.ts","../src/store/keys.ts"],"sourcesContent":["// Generated by tools/stamp-version.mjs from @fewchurs/react's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.0.1\";\n","import type { FeatureStatus } from \"../types\";\nimport { SDK_VERSION } from \"./version\";\n\nexport { SDK_VERSION };\nexport const SDK_HEADER = `react/${SDK_VERSION}`;\nexport const DEFAULT_BASE_URL = \"https://www.fewchurs.com/api/v1\";\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */\nexport const LIMITS = {\n titleMin: 3,\n titleMax: 100,\n descriptionMax: 1000,\n commentMin: 1,\n commentMax: 1000,\n authorNameMax: 60,\n} as const;\n\n/** Deliberately loose: the server owns the real check, this only catches obvious typos. */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */\nexport const ROADMAP_STATUSES: FeatureStatus[] = [\"planned\", \"in_progress\", \"done\"];\n","export type FewchursError =\n | { code: \"invalid_key\"; status: 401; message: string }\n | { code: \"validation_failed\"; status: 400; message: string }\n | { code: \"rate_limited\"; status: 429; message: string }\n | { code: \"not_found\"; status: 404; message: string }\n | { code: \"limit_reached\"; status: 402; message: string }\n | { code: \"server\"; status: number; message: string }\n | { code: \"network\"; message: string };\n\nexport type Result<T> = { ok: true; data: T } | { ok: false; error: FewchursError };\n\ninterface ServerErrorBody {\n error?: { code?: string; message?: string };\n}\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Feedback is unavailable right now.\",\n validation_failed: \"Please check what you entered and try again.\",\n rate_limited: \"You have reached today's limit. Try again tomorrow.\",\n not_found: \"That request no longer exists.\",\n limit_reached: \"Feedback is unavailable right now.\",\n server: \"Something went wrong. Please try again.\",\n network: \"Could not reach the server. Check your connection.\",\n};\n\n/** Safe to show a visitor: never leaks a server message that may name internals. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n return USER_MESSAGES[error.code];\n}\n\nexport function networkError(message = \"Network request failed.\"): FewchursError {\n return { code: \"network\", message };\n}\n\nexport function validationError(message: string): FewchursError {\n return { code: \"validation_failed\", status: 400, message };\n}\n\nexport function errorFromResponse(status: number, body: unknown): FewchursError {\n const code = (body as ServerErrorBody | null)?.error?.code;\n const message = (body as ServerErrorBody | null)?.error?.message ?? `Request failed (${status}).`;\n\n switch (code) {\n case \"invalid_key\":\n return { code: \"invalid_key\", status: 401, message };\n // missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,\n // so it collapses into validation_failed rather than widening the public union.\n case \"missing_device_id\":\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message };\n case \"rate_limited\":\n return { code: \"rate_limited\", status: 429, message };\n case \"not_found\":\n return { code: \"not_found\", status: 404, message };\n case \"limit_reached\":\n return { code: \"limit_reached\", status: 402, message };\n default:\n return status === 401\n ? { code: \"invalid_key\", status: 401, message }\n : { code: \"server\", status, message };\n }\n}\n","import type {\n Comment,\n CommentPage,\n Feature,\n FeaturePage,\n FeatureStatus,\n FewchursConfig,\n} from \"../types\";\n\nconst KNOWN_STATUSES = new Set<string>([\"open\", \"planned\", \"in_progress\", \"done\", \"declined\"]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : {};\n}\n\nfunction str(value: unknown, fallback = \"\"): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\nfunction bool(value: unknown): boolean {\n return value === true;\n}\n\nexport function normalizeStatus(value: unknown): FeatureStatus {\n return typeof value === \"string\" && KNOWN_STATUSES.has(value)\n ? (value as FeatureStatus)\n : \"unknown\";\n}\n\n// `isMine` is the server's answer once it ships; until then the raw author device id is the\n// only signal, so it is compared here and never surfaced on the public type.\nfunction isMine(raw: Record<string, unknown>, ownerKey: string, deviceId: string | null): boolean {\n if (typeof raw.isMine === \"boolean\") return raw.isMine;\n const owner = raw[ownerKey];\n return typeof owner === \"string\" && owner.length > 0 && owner === deviceId;\n}\n\nexport function normalizeFeature(value: unknown, deviceId: string | null): Feature {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n title: str(raw.title),\n description: str(raw.description),\n status: normalizeStatus(raw.status),\n upvoteCount: num(raw.upvoteCount),\n commentCount: num(raw.commentCount),\n followerCount: num(raw.followerCount),\n hasVoted: bool(raw.hasVoted),\n isFollowing: bool(raw.isFollowing),\n isMine: isMine(raw, \"authorDeviceId\", deviceId),\n createdAt: str(raw.createdAt),\n updatedAt: str(raw.updatedAt, str(raw.createdAt)),\n };\n}\n\nexport function normalizeComment(value: unknown, deviceId: string | null): Comment {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n text: str(raw.text),\n authorName: str(raw.authorName, \"App User\"),\n isDeveloper: bool(raw.isDeveloper),\n isMine: isMine(raw, \"deviceId\", deviceId),\n createdAt: str(raw.createdAt),\n };\n}\n\nexport function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage {\n const raw = asRecord(value);\n const features = Array.isArray(raw.features) ? raw.features : [];\n return {\n features: features.map((feature) => normalizeFeature(feature, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage {\n const raw = asRecord(value);\n const comments = Array.isArray(raw.comments) ? raw.comments : [];\n return {\n comments: comments.map((comment) => normalizeComment(comment, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeConfig(value: unknown): FewchursConfig {\n const raw = asRecord(value);\n return {\n showBranding: raw.showBranding !== false,\n hideVoteCounts: bool(raw.hideVoteCounts),\n appName: typeof raw.appName === \"string\" && raw.appName ? raw.appName : null,\n };\n}\n\nexport function normalizeVote(value: unknown): { upvoteCount: number; hasVoted: boolean } {\n const raw = asRecord(value);\n return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };\n}\n","import { EMAIL_PATTERN, LIMITS } from \"./constants\";\nimport { validationError, type FewchursError } from \"./errors\";\nimport type { CreateCommentInput, SubmitFeatureInput } from \"../types\";\n\nexport function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null {\n const title = input.title.trim();\n if (title.length < LIMITS.titleMin) {\n return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);\n }\n if (title.length > LIMITS.titleMax) {\n return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);\n }\n if ((input.description ?? \"\").trim().length > LIMITS.descriptionMax) {\n return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);\n }\n if (input.email !== undefined && input.email !== \"\" && !isEmail(input.email)) {\n return validationError(\"Enter a valid email address.\");\n }\n return null;\n}\n\nexport function validateCommentInput(input: CreateCommentInput): FewchursError | null {\n const text = input.text.trim();\n if (text.length < LIMITS.commentMin) return validationError(\"Write a comment first.\");\n if (text.length > LIMITS.commentMax) {\n return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);\n }\n if ((input.authorName ?? \"\").trim().length > LIMITS.authorNameMax) {\n return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);\n }\n return null;\n}\n\nexport function validateEmail(email: string): FewchursError | null {\n return isEmail(email) ? null : validationError(\"Enter a valid email address.\");\n}\n\nfunction isEmail(email: string): boolean {\n return EMAIL_PATTERN.test(email.trim());\n}\n","import { DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, SDK_HEADER } from \"./constants\";\nimport { errorFromResponse, networkError, type Result } from \"./errors\";\nimport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeVote,\n} from \"./normalize\";\nimport { validateCommentInput, validateEmail, validateFeatureInput } from \"./validate\";\nimport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"../types\";\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface FewchursClientOptions {\n /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */\n apiKey?: string;\n /** `https://www.fewchurs.com/api/v1` by default, or a proxy path such as `/api/fewchurs`. */\n baseUrl?: string;\n deviceId?: string | null | (() => string | null);\n /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */\n language?: string;\n fetch?: FetchLike;\n retries?: number;\n retryDelayMs?: number;\n}\n\nexport interface FewchursClient {\n readonly baseUrl: string;\n getDeviceId(): string | null;\n getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;\n listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;\n getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;\n submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;\n vote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n unvote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n follow(featureId: string, email: string): Promise<Result<{ following: boolean }>>;\n unfollow(featureId: string): Promise<Result<{ following: boolean }>>;\n listComments(\n featureId: string,\n params?: ListCommentsParams,\n signal?: AbortSignal,\n ): Promise<Result<CommentPage>>;\n createComment(input: CreateCommentInput): Promise<Result<Comment>>;\n}\n\ninterface RequestOptions {\n method?: \"GET\" | \"POST\" | \"DELETE\";\n body?: unknown;\n query?: Record<string, string | number | undefined | null>;\n signal?: AbortSignal;\n}\n\nexport function createFewchursClient(options: FewchursClientOptions = {}): FewchursClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);\n const retries = options.retries ?? 1;\n const retryDelayMs = options.retryDelayMs ?? 250;\n\n function getDeviceId(): string | null {\n return typeof options.deviceId === \"function\" ? options.deviceId() : (options.deviceId ?? null);\n }\n\n function headers(hasBody: boolean): Record<string, string> {\n const result: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Fewchurs-Sdk\": SDK_HEADER,\n };\n if (hasBody) result[\"Content-Type\"] = \"application/json\";\n if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;\n const deviceId = getDeviceId();\n if (deviceId) result[\"X-Device-Id\"] = deviceId;\n if (options.language) result[\"Accept-Language\"] = options.language;\n return result;\n }\n\n function url(path: string, query?: RequestOptions[\"query\"]): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== null && value !== \"\") search.set(key, String(value));\n }\n const qs = search.toString();\n return `${baseUrl}${path}${qs ? `?${qs}` : \"\"}`;\n }\n\n async function request<T>(\n path: string,\n map: (body: unknown) => T,\n { method = \"GET\", body, query, signal }: RequestOptions = {},\n ): Promise<Result<T>> {\n if (!doFetch) return { ok: false, error: networkError(\"No fetch implementation available.\") };\n\n const target = url(path, query);\n const init: RequestInit = { method, headers: headers(body !== undefined) };\n if (body !== undefined) init.body = JSON.stringify(body);\n if (signal) init.signal = signal;\n\n // Only GETs are retried: a retried POST could double-submit a request or a comment.\n const attempts = method === \"GET\" ? retries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n let response: Response;\n try {\n response = await doFetch(target, init);\n } catch (error) {\n if (signal?.aborted) return { ok: false, error: networkError(\"Request aborted.\") };\n if (attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n return { ok: false, error: networkError(messageOf(error)) };\n }\n\n if (response.ok) {\n const payload = await readJson(response);\n return { ok: true, data: map(payload) };\n }\n\n if (response.status >= 500 && attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n\n const failure = errorFromResponse(response.status, await readJson(response));\n warnAboutKey(failure.code);\n return { ok: false, error: failure };\n }\n\n return { ok: false, error: networkError() };\n }\n\n return {\n baseUrl,\n getDeviceId,\n\n getConfig: (signal) => request(\"/config\", normalizeConfig, { signal }),\n\n listFeatures: (params = {}, signal) =>\n request(\"/features\", (body) => normalizeFeaturePage(body, getDeviceId()), {\n query: {\n sort: params.sort ?? \"new\",\n limit: params.limit ?? DEFAULT_PAGE_SIZE,\n cursor: params.cursor ?? undefined,\n status: params.status?.length ? params.status.join(\",\") : undefined,\n },\n signal,\n }),\n\n getFeature: (featureId, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}`,\n (body) => normalizeFeature(body, getDeviceId()),\n { signal },\n ),\n\n submitFeature: async (input) => {\n const invalid = validateFeatureInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/features\", (body) => normalizeFeature(body, getDeviceId()), {\n method: \"POST\",\n body: {\n title: input.title.trim(),\n description: (input.description ?? \"\").trim(),\n ...(input.email ? { email: input.email.trim() } : {}),\n isSubscriber: input.isSubscriber ?? false,\n },\n });\n },\n\n vote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: \"POST\" }),\n\n unvote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {\n method: \"DELETE\",\n }),\n\n follow: async (featureId, email) => {\n const invalid = validateEmail(email);\n if (invalid) return { ok: false, error: invalid };\n return request(\n `/features/${encodeURIComponent(featureId)}/follow`,\n () => ({ following: true }),\n {\n method: \"POST\",\n body: { email: email.trim() },\n },\n );\n },\n\n unfollow: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {\n method: \"DELETE\",\n }),\n\n listComments: (featureId, params = {}, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}/comments`,\n (body) => normalizeCommentPage(body, getDeviceId()),\n {\n query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? undefined },\n signal,\n },\n ),\n\n // The server takes the feature id in the body here; POST /features/{id}/comments is a 405.\n createComment: async (input) => {\n const invalid = validateCommentInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/comments\", (body) => normalizeComment(body, getDeviceId()), {\n method: \"POST\",\n body: {\n featureId: input.featureId,\n text: input.text.trim(),\n ...(input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}),\n },\n });\n },\n };\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : \"Network request failed.\";\n}\n\n// Declared, not imported from @types/node: this is a browser package, and bundlers replace\n// `process.env.NODE_ENV` inline, so the check costs nothing in a production build.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet warnedAboutKey = false;\n\nfunction warnAboutKey(code: string): void {\n if (code !== \"invalid_key\" || warnedAboutKey) return;\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n warnedAboutKey = true;\n console.error(\n \"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard.\",\n );\n}\n","const COOKIE_NAME = \"fewchurs_did\";\nconst STORAGE_KEY = \"fewchurs_device_id\";\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nexport function createDeviceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport function readDeviceId(): string | null {\n if (typeof document === \"undefined\") return null;\n return readCookie() ?? readStorage();\n}\n\n/**\n * Cookie first, storage second: a cookie is the only copy a same-origin server can read, so\n * the board renders with the same identity on both sides and never mismatches on hydration.\n */\nexport function resolveDeviceId(): string {\n const existing = readDeviceId();\n if (existing) {\n persist(existing);\n return existing;\n }\n const created = createDeviceId();\n persist(created);\n return created;\n}\n\nfunction persist(deviceId: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${COOKIE_NAME}=${encodeURIComponent(deviceId)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;\n } catch {\n // Cookies can be blocked entirely; storage below is the fallback.\n }\n try {\n localStorage.setItem(STORAGE_KEY, deviceId);\n } catch {\n // Private-mode storage throws on write; the cookie or a fresh id per visit still works.\n }\n}\n\nfunction readCookie(): string | null {\n const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction readStorage(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nexport { COOKIE_NAME as DEVICE_ID_COOKIE };\n","import type { FeatureStatus, FeatureSort } from \"../types\";\n\nexport interface ListQuery {\n sort: FeatureSort;\n status?: FeatureStatus[];\n}\n\nexport function listKey({ sort, status }: ListQuery): string {\n return status?.length ? `${sort}:${[...status].sort().join(\",\")}` : sort;\n}\n\n/**\n * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked\n * request goes through the same vote, comment and follow updates as a row in a list.\n */\nexport function featureKey(featureId: string): string {\n return `feature:${featureId}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client/version.ts","../src/client/constants.ts","../src/client/errors.ts","../src/client/normalize.ts","../src/client/validate.ts","../src/client/client.ts","../src/identity/deviceId.ts","../src/store/keys.ts"],"sourcesContent":["// Generated by tools/stamp-version.mjs from @fewchurs/react's package.json. Do not edit by hand.\nexport const SDK_VERSION = \"1.1.0\";\n","import type { FeatureStatus } from \"../types\";\nimport { SDK_VERSION } from \"./version\";\n\nexport { SDK_VERSION };\nexport const SDK_HEADER = `react/${SDK_VERSION}`;\nexport const DEFAULT_BASE_URL = \"https://www.fewchurs.com/api/v1\";\nexport const DEFAULT_PAGE_SIZE = 20;\n\n/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */\nexport const LIMITS = {\n titleMin: 3,\n titleMax: 100,\n descriptionMax: 1000,\n commentMin: 1,\n commentMax: 1000,\n authorNameMax: 60,\n} as const;\n\n/** Deliberately loose: the server owns the real check, this only catches obvious typos. */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */\nexport const ROADMAP_STATUSES: FeatureStatus[] = [\"planned\", \"in_progress\", \"done\"];\n","export type FewchursError =\n | { code: \"invalid_key\"; status: 401 | 403; message: string }\n | { code: \"validation_failed\"; status: 400; message: string }\n | { code: \"rate_limited\"; status: 429; message: string }\n | { code: \"not_found\"; status: 404; message: string }\n | { code: \"limit_reached\"; status: 402; message: string }\n | { code: \"server\"; status: number; message: string }\n | { code: \"network\"; message: string };\n\nexport type Result<T> = { ok: true; data: T } | { ok: false; error: FewchursError };\n\ninterface ServerErrorBody {\n error?: { code?: string; message?: string };\n}\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Feedback is unavailable right now.\",\n validation_failed: \"Please check what you entered and try again.\",\n rate_limited: \"You have reached today's limit. Try again tomorrow.\",\n not_found: \"That request no longer exists.\",\n limit_reached: \"Feedback is unavailable right now.\",\n server: \"Something went wrong. Please try again.\",\n network: \"Could not reach the server. Check your connection.\",\n};\n\n/** Safe to show a visitor: never leaks a server message that may name internals. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n return USER_MESSAGES[error.code];\n}\n\nexport function networkError(message = \"Network request failed.\"): FewchursError {\n return { code: \"network\", message };\n}\n\nexport function validationError(message: string): FewchursError {\n return { code: \"validation_failed\", status: 400, message };\n}\n\nexport function errorFromResponse(status: number, body: unknown): FewchursError {\n const code = (body as ServerErrorBody | null)?.error?.code;\n const message = (body as ServerErrorBody | null)?.error?.message ?? `Request failed (${status}).`;\n\n switch (code) {\n case \"invalid_key\":\n return { code: \"invalid_key\", status: 401, message };\n case \"wrong_app\":\n return { code: \"invalid_key\", status: 403, message };\n // missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,\n // so it collapses into validation_failed rather than widening the public union.\n case \"missing_device_id\":\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message };\n case \"rate_limited\":\n return { code: \"rate_limited\", status: 429, message };\n case \"not_found\":\n return { code: \"not_found\", status: 404, message };\n case \"limit_reached\":\n return { code: \"limit_reached\", status: 402, message };\n default:\n return status === 401\n ? { code: \"invalid_key\", status: 401, message }\n : { code: \"server\", status, message };\n }\n}\n","import type {\n Comment,\n CommentPage,\n Feature,\n FeaturePage,\n FeatureStatus,\n FewchursConfig,\n} from \"../types\";\n\nconst KNOWN_STATUSES = new Set<string>([\"open\", \"planned\", \"in_progress\", \"done\", \"declined\"]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : {};\n}\n\nfunction str(value: unknown, fallback = \"\"): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction num(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\nfunction bool(value: unknown): boolean {\n return value === true;\n}\n\nexport function normalizeStatus(value: unknown): FeatureStatus {\n return typeof value === \"string\" && KNOWN_STATUSES.has(value)\n ? (value as FeatureStatus)\n : \"unknown\";\n}\n\n// `isMine` is the server's answer once it ships; until then the raw author device id is the\n// only signal, so it is compared here and never surfaced on the public type.\nfunction isMine(raw: Record<string, unknown>, ownerKey: string, deviceId: string | null): boolean {\n if (typeof raw.isMine === \"boolean\") return raw.isMine;\n const owner = raw[ownerKey];\n return typeof owner === \"string\" && owner.length > 0 && owner === deviceId;\n}\n\nexport function normalizeFeature(value: unknown, deviceId: string | null): Feature {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n title: str(raw.title),\n description: str(raw.description),\n status: normalizeStatus(raw.status),\n upvoteCount: num(raw.upvoteCount),\n commentCount: num(raw.commentCount),\n followerCount: num(raw.followerCount),\n hasVoted: bool(raw.hasVoted),\n isFollowing: bool(raw.isFollowing),\n isMine: isMine(raw, \"authorDeviceId\", deviceId),\n createdAt: str(raw.createdAt),\n updatedAt: str(raw.updatedAt, str(raw.createdAt)),\n };\n}\n\nexport function normalizeComment(value: unknown, deviceId: string | null): Comment {\n const raw = asRecord(value);\n return {\n id: str(raw.id),\n text: str(raw.text),\n authorName: str(raw.authorName, \"App User\"),\n isDeveloper: bool(raw.isDeveloper),\n isMine: isMine(raw, \"deviceId\", deviceId),\n createdAt: str(raw.createdAt),\n };\n}\n\nexport function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage {\n const raw = asRecord(value);\n const features = Array.isArray(raw.features) ? raw.features : [];\n return {\n features: features.map((feature) => normalizeFeature(feature, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage {\n const raw = asRecord(value);\n const comments = Array.isArray(raw.comments) ? raw.comments : [];\n return {\n comments: comments.map((comment) => normalizeComment(comment, deviceId)),\n nextCursor: typeof raw.nextCursor === \"string\" ? raw.nextCursor : null,\n };\n}\n\nexport function normalizeConfig(value: unknown): FewchursConfig {\n const raw = asRecord(value);\n return {\n showBranding: raw.showBranding !== false,\n hideVoteCounts: bool(raw.hideVoteCounts),\n appName: typeof raw.appName === \"string\" && raw.appName ? raw.appName : null,\n };\n}\n\nexport function normalizeVote(value: unknown): { upvoteCount: number; hasVoted: boolean } {\n const raw = asRecord(value);\n return { upvoteCount: num(raw.upvoteCount), hasVoted: bool(raw.hasVoted) };\n}\n","import { EMAIL_PATTERN, LIMITS } from \"./constants\";\nimport { validationError, type FewchursError } from \"./errors\";\nimport type { CreateCommentInput, SubmitFeatureInput } from \"../types\";\n\nexport function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null {\n const title = input.title.trim();\n if (title.length < LIMITS.titleMin) {\n return validationError(`Title must be at least ${LIMITS.titleMin} characters.`);\n }\n if (title.length > LIMITS.titleMax) {\n return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);\n }\n if ((input.description ?? \"\").trim().length > LIMITS.descriptionMax) {\n return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);\n }\n if (input.email !== undefined && input.email !== \"\" && !isEmail(input.email)) {\n return validationError(\"Enter a valid email address.\");\n }\n return null;\n}\n\nexport function validateCommentInput(input: CreateCommentInput): FewchursError | null {\n const text = input.text.trim();\n if (text.length < LIMITS.commentMin) return validationError(\"Write a comment first.\");\n if (text.length > LIMITS.commentMax) {\n return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);\n }\n if ((input.authorName ?? \"\").trim().length > LIMITS.authorNameMax) {\n return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);\n }\n return null;\n}\n\nexport function validateEmail(email: string): FewchursError | null {\n return isEmail(email) ? null : validationError(\"Enter a valid email address.\");\n}\n\nfunction isEmail(email: string): boolean {\n return EMAIL_PATTERN.test(email.trim());\n}\n","import { DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, SDK_HEADER } from \"./constants\";\nimport { errorFromResponse, networkError, type FewchursError, type Result } from \"./errors\";\nimport {\n normalizeComment,\n normalizeCommentPage,\n normalizeConfig,\n normalizeFeature,\n normalizeFeaturePage,\n normalizeVote,\n} from \"./normalize\";\nimport { validateCommentInput, validateEmail, validateFeatureInput } from \"./validate\";\nimport type {\n Comment,\n CommentPage,\n CreateCommentInput,\n Feature,\n FeaturePage,\n FewchursConfig,\n ListCommentsParams,\n ListFeaturesParams,\n SubmitFeatureInput,\n} from \"../types\";\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface FewchursClientOptions {\n /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */\n apiKey?: string;\n /** `https://www.fewchurs.com/api/v1` by default, or a proxy path such as `/api/fewchurs`. */\n baseUrl?: string;\n deviceId?: string | null | (() => string | null);\n /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */\n language?: string;\n /**\n * The domain the API key is checked against, for calls made from a server. A browser reports\n * its own domain through `Origin`, which always wins, so leave this out in a SPA.\n */\n appId?: string;\n fetch?: FetchLike;\n retries?: number;\n retryDelayMs?: number;\n}\n\nexport interface FewchursClient {\n readonly baseUrl: string;\n getDeviceId(): string | null;\n getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;\n listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;\n getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;\n submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;\n vote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n unvote(featureId: string): Promise<Result<{ upvoteCount: number; hasVoted: boolean }>>;\n follow(featureId: string, email: string): Promise<Result<{ following: boolean }>>;\n unfollow(featureId: string): Promise<Result<{ following: boolean }>>;\n listComments(\n featureId: string,\n params?: ListCommentsParams,\n signal?: AbortSignal,\n ): Promise<Result<CommentPage>>;\n createComment(input: CreateCommentInput): Promise<Result<Comment>>;\n}\n\ninterface RequestOptions {\n method?: \"GET\" | \"POST\" | \"DELETE\";\n body?: unknown;\n query?: Record<string, string | number | undefined | null>;\n signal?: AbortSignal;\n}\n\nexport function createFewchursClient(options: FewchursClientOptions = {}): FewchursClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);\n const retries = options.retries ?? 1;\n const retryDelayMs = options.retryDelayMs ?? 250;\n\n function getDeviceId(): string | null {\n return typeof options.deviceId === \"function\" ? options.deviceId() : (options.deviceId ?? null);\n }\n\n function headers(hasBody: boolean): Record<string, string> {\n const result: Record<string, string> = {\n Accept: \"application/json\",\n \"X-Fewchurs-Sdk\": SDK_HEADER,\n };\n if (hasBody) result[\"Content-Type\"] = \"application/json\";\n if (options.apiKey) result.Authorization = `Bearer ${options.apiKey}`;\n const deviceId = getDeviceId();\n if (deviceId) result[\"X-Device-Id\"] = deviceId;\n if (options.language) result[\"Accept-Language\"] = options.language;\n if (options.appId) result[\"X-Fewchurs-App-Id\"] = options.appId;\n return result;\n }\n\n function url(path: string, query?: RequestOptions[\"query\"]): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined && value !== null && value !== \"\") search.set(key, String(value));\n }\n const qs = search.toString();\n return `${baseUrl}${path}${qs ? `?${qs}` : \"\"}`;\n }\n\n async function request<T>(\n path: string,\n map: (body: unknown) => T,\n { method = \"GET\", body, query, signal }: RequestOptions = {},\n ): Promise<Result<T>> {\n if (!doFetch) return { ok: false, error: networkError(\"No fetch implementation available.\") };\n\n const target = url(path, query);\n const init: RequestInit = { method, headers: headers(body !== undefined) };\n if (body !== undefined) init.body = JSON.stringify(body);\n if (signal) init.signal = signal;\n\n // Only GETs are retried: a retried POST could double-submit a request or a comment.\n const attempts = method === \"GET\" ? retries + 1 : 1;\n\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n let response: Response;\n try {\n response = await doFetch(target, init);\n } catch (error) {\n if (signal?.aborted) return { ok: false, error: networkError(\"Request aborted.\") };\n if (attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n return { ok: false, error: networkError(messageOf(error)) };\n }\n\n if (response.ok) {\n const payload = await readJson(response);\n return { ok: true, data: map(payload) };\n }\n\n if (response.status >= 500 && attempt < attempts - 1) {\n await delay(retryDelayMs);\n continue;\n }\n\n const failure = errorFromResponse(response.status, await readJson(response));\n warnAboutKey(failure);\n return { ok: false, error: failure };\n }\n\n return { ok: false, error: networkError() };\n }\n\n return {\n baseUrl,\n getDeviceId,\n\n getConfig: (signal) => request(\"/config\", normalizeConfig, { signal }),\n\n listFeatures: (params = {}, signal) =>\n request(\"/features\", (body) => normalizeFeaturePage(body, getDeviceId()), {\n query: {\n sort: params.sort ?? \"new\",\n limit: params.limit ?? DEFAULT_PAGE_SIZE,\n cursor: params.cursor ?? undefined,\n status: params.status?.length ? params.status.join(\",\") : undefined,\n },\n signal,\n }),\n\n getFeature: (featureId, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}`,\n (body) => normalizeFeature(body, getDeviceId()),\n { signal },\n ),\n\n submitFeature: async (input) => {\n const invalid = validateFeatureInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/features\", (body) => normalizeFeature(body, getDeviceId()), {\n method: \"POST\",\n body: {\n title: input.title.trim(),\n description: (input.description ?? \"\").trim(),\n ...(input.email ? { email: input.email.trim() } : {}),\n isSubscriber: input.isSubscriber ?? false,\n },\n });\n },\n\n vote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, { method: \"POST\" }),\n\n unvote: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/vote`, normalizeVote, {\n method: \"DELETE\",\n }),\n\n follow: async (featureId, email) => {\n const invalid = validateEmail(email);\n if (invalid) return { ok: false, error: invalid };\n return request(\n `/features/${encodeURIComponent(featureId)}/follow`,\n () => ({ following: true }),\n {\n method: \"POST\",\n body: { email: email.trim() },\n },\n );\n },\n\n unfollow: (featureId) =>\n request(`/features/${encodeURIComponent(featureId)}/follow`, () => ({ following: false }), {\n method: \"DELETE\",\n }),\n\n listComments: (featureId, params = {}, signal) =>\n request(\n `/features/${encodeURIComponent(featureId)}/comments`,\n (body) => normalizeCommentPage(body, getDeviceId()),\n {\n query: { limit: params.limit ?? DEFAULT_PAGE_SIZE, cursor: params.cursor ?? undefined },\n signal,\n },\n ),\n\n // The server takes the feature id in the body here; POST /features/{id}/comments is a 405.\n createComment: async (input) => {\n const invalid = validateCommentInput(input);\n if (invalid) return { ok: false, error: invalid };\n return request(\"/comments\", (body) => normalizeComment(body, getDeviceId()), {\n method: \"POST\",\n body: {\n featureId: input.featureId,\n text: input.text.trim(),\n ...(input.authorName?.trim() ? { authorName: input.authorName.trim() } : {}),\n },\n });\n },\n };\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : \"Network request failed.\";\n}\n\n// Declared, not imported from @types/node: this is a browser package, and bundlers replace\n// `process.env.NODE_ENV` inline, so the check costs nothing in a production build.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet warnedAboutKey = false;\n\nfunction warnAboutKey(error: FewchursError): void {\n if (error.code !== \"invalid_key\" || warnedAboutKey) return;\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n warnedAboutKey = true;\n console.error(\n error.status === 403\n ? `[fewchurs] ${error.message}`\n : \"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard.\",\n );\n}\n","const COOKIE_NAME = \"fewchurs_did\";\nconst STORAGE_KEY = \"fewchurs_device_id\";\nconst COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\nexport function createDeviceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `fw-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport function readDeviceId(): string | null {\n if (typeof document === \"undefined\") return null;\n return readCookie() ?? readStorage();\n}\n\n/**\n * Cookie first, storage second: a cookie is the only copy a same-origin server can read, so\n * the board renders with the same identity on both sides and never mismatches on hydration.\n */\nexport function resolveDeviceId(): string {\n const existing = readDeviceId();\n if (existing) {\n persist(existing);\n return existing;\n }\n const created = createDeviceId();\n persist(created);\n return created;\n}\n\nfunction persist(deviceId: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${COOKIE_NAME}=${encodeURIComponent(deviceId)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;\n } catch {\n // Cookies can be blocked entirely; storage below is the fallback.\n }\n try {\n localStorage.setItem(STORAGE_KEY, deviceId);\n } catch {\n // Private-mode storage throws on write; the cookie or a fresh id per visit still works.\n }\n}\n\nfunction readCookie(): string | null {\n const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));\n return match?.[1] ? decodeURIComponent(match[1]) : null;\n}\n\nfunction readStorage(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nexport { COOKIE_NAME as DEVICE_ID_COOKIE };\n","import type { FeatureStatus, FeatureSort } from \"../types\";\n\nexport interface ListQuery {\n sort: FeatureSort;\n status?: FeatureStatus[];\n}\n\nexport function listKey({ sort, status }: ListQuery): string {\n return status?.length ? `${sort}:${[...status].sort().join(\",\")}` : sort;\n}\n\n/**\n * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked\n * request goes through the same vote, comment and follow updates as a row in a list.\n */\nexport function featureKey(featureId: string): string {\n return `feature:${featureId}`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { ReactNode, CSSProperties } from 'react';
|
|
|
2
2
|
|
|
3
3
|
type FewchursError = {
|
|
4
4
|
code: "invalid_key";
|
|
5
|
-
status: 401;
|
|
5
|
+
status: 401 | 403;
|
|
6
6
|
message: string;
|
|
7
7
|
} | {
|
|
8
8
|
code: "validation_failed";
|
|
@@ -110,6 +110,11 @@ interface FewchursClientOptions {
|
|
|
110
110
|
deviceId?: string | null | (() => string | null);
|
|
111
111
|
/** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
|
|
112
112
|
language?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The domain the API key is checked against, for calls made from a server. A browser reports
|
|
115
|
+
* its own domain through `Origin`, which always wins, so leave this out in a SPA.
|
|
116
|
+
*/
|
|
117
|
+
appId?: string;
|
|
113
118
|
fetch?: FetchLike;
|
|
114
119
|
retries?: number;
|
|
115
120
|
retryDelayMs?: number;
|
|
@@ -308,9 +313,9 @@ declare function useBoard(): BoardContextValue;
|
|
|
308
313
|
|
|
309
314
|
declare function Branding(): React.JSX.Element | null;
|
|
310
315
|
|
|
311
|
-
declare const SDK_VERSION = "1.0
|
|
316
|
+
declare const SDK_VERSION = "1.1.0";
|
|
312
317
|
|
|
313
|
-
declare const SDK_HEADER = "react/1.0
|
|
318
|
+
declare const SDK_HEADER = "react/1.1.0";
|
|
314
319
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
315
320
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
316
321
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ReactNode, CSSProperties } from 'react';
|
|
|
2
2
|
|
|
3
3
|
type FewchursError = {
|
|
4
4
|
code: "invalid_key";
|
|
5
|
-
status: 401;
|
|
5
|
+
status: 401 | 403;
|
|
6
6
|
message: string;
|
|
7
7
|
} | {
|
|
8
8
|
code: "validation_failed";
|
|
@@ -110,6 +110,11 @@ interface FewchursClientOptions {
|
|
|
110
110
|
deviceId?: string | null | (() => string | null);
|
|
111
111
|
/** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
|
|
112
112
|
language?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The domain the API key is checked against, for calls made from a server. A browser reports
|
|
115
|
+
* its own domain through `Origin`, which always wins, so leave this out in a SPA.
|
|
116
|
+
*/
|
|
117
|
+
appId?: string;
|
|
113
118
|
fetch?: FetchLike;
|
|
114
119
|
retries?: number;
|
|
115
120
|
retryDelayMs?: number;
|
|
@@ -308,9 +313,9 @@ declare function useBoard(): BoardContextValue;
|
|
|
308
313
|
|
|
309
314
|
declare function Branding(): React.JSX.Element | null;
|
|
310
315
|
|
|
311
|
-
declare const SDK_VERSION = "1.0
|
|
316
|
+
declare const SDK_VERSION = "1.1.0";
|
|
312
317
|
|
|
313
|
-
declare const SDK_HEADER = "react/1.0
|
|
318
|
+
declare const SDK_HEADER = "react/1.1.0";
|
|
314
319
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
315
320
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
316
321
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
84
84
|
var import_react = require("react");
|
|
85
85
|
|
|
86
86
|
// src/client/version.ts
|
|
87
|
-
var SDK_VERSION = "1.0
|
|
87
|
+
var SDK_VERSION = "1.1.0";
|
|
88
88
|
|
|
89
89
|
// src/client/constants.ts
|
|
90
90
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -127,6 +127,8 @@ function errorFromResponse(status, body) {
|
|
|
127
127
|
switch (code) {
|
|
128
128
|
case "invalid_key":
|
|
129
129
|
return { code: "invalid_key", status: 401, message };
|
|
130
|
+
case "wrong_app":
|
|
131
|
+
return { code: "invalid_key", status: 403, message };
|
|
130
132
|
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
131
133
|
// so it collapses into validation_failed rather than widening the public union.
|
|
132
134
|
case "missing_device_id":
|
|
@@ -276,6 +278,7 @@ function createFewchursClient(options = {}) {
|
|
|
276
278
|
const deviceId = getDeviceId();
|
|
277
279
|
if (deviceId) result["X-Device-Id"] = deviceId;
|
|
278
280
|
if (options.language) result["Accept-Language"] = options.language;
|
|
281
|
+
if (options.appId) result["X-Fewchurs-App-Id"] = options.appId;
|
|
279
282
|
return result;
|
|
280
283
|
}
|
|
281
284
|
function url(path, query) {
|
|
@@ -314,7 +317,7 @@ function createFewchursClient(options = {}) {
|
|
|
314
317
|
continue;
|
|
315
318
|
}
|
|
316
319
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
317
|
-
warnAboutKey(failure
|
|
320
|
+
warnAboutKey(failure);
|
|
318
321
|
return { ok: false, error: failure };
|
|
319
322
|
}
|
|
320
323
|
return { ok: false, error: networkError() };
|
|
@@ -406,12 +409,12 @@ function messageOf(error) {
|
|
|
406
409
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
407
410
|
}
|
|
408
411
|
var warnedAboutKey = false;
|
|
409
|
-
function warnAboutKey(
|
|
410
|
-
if (code !== "invalid_key" || warnedAboutKey) return;
|
|
412
|
+
function warnAboutKey(error) {
|
|
413
|
+
if (error.code !== "invalid_key" || warnedAboutKey) return;
|
|
411
414
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
412
415
|
warnedAboutKey = true;
|
|
413
416
|
console.error(
|
|
414
|
-
"[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
417
|
+
error.status === 403 ? `[fewchurs] ${error.message}` : "[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
|
|
415
418
|
);
|
|
416
419
|
}
|
|
417
420
|
|