@fewchurs/react 1.3.0 → 1.5.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/dist/core.d.mts +5 -4
- package/dist/core.d.ts +5 -4
- package/dist/core.js +52 -29
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +51 -29
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +152 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -70
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +41 -0
- package/package.json +1 -1
package/dist/core.d.mts
CHANGED
|
@@ -33,7 +33,8 @@ type Result<T> = {
|
|
|
33
33
|
ok: false;
|
|
34
34
|
error: FewchursError;
|
|
35
35
|
};
|
|
36
|
-
|
|
36
|
+
declare const NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
37
|
+
/** Safe to show a visitor. Only validation and `wrong_app` use the server's own text. */
|
|
37
38
|
declare function userMessage(error: FewchursError): string;
|
|
38
39
|
declare function networkError(message?: string): FewchursError;
|
|
39
40
|
declare function validationError(message: string): FewchursError;
|
|
@@ -143,9 +144,9 @@ interface FewchursClient {
|
|
|
143
144
|
}
|
|
144
145
|
declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
|
|
145
146
|
|
|
146
|
-
declare const SDK_VERSION = "1.
|
|
147
|
+
declare const SDK_VERSION = "1.5.0";
|
|
147
148
|
|
|
148
|
-
declare const SDK_HEADER = "react/1.
|
|
149
|
+
declare const SDK_HEADER = "react/1.5.0";
|
|
149
150
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
150
151
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
151
152
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
|
@@ -215,4 +216,4 @@ declare function validateFeatureInput(input: SubmitFeatureInput): FewchursError
|
|
|
215
216
|
declare function validateCommentInput(input: CreateCommentInput): FewchursError | null;
|
|
216
217
|
declare function validateEmail(email: string): FewchursError | null;
|
|
217
218
|
|
|
218
|
-
export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };
|
|
219
|
+
export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, NOT_CONFIGURED_MESSAGE, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };
|
package/dist/core.d.ts
CHANGED
|
@@ -33,7 +33,8 @@ type Result<T> = {
|
|
|
33
33
|
ok: false;
|
|
34
34
|
error: FewchursError;
|
|
35
35
|
};
|
|
36
|
-
|
|
36
|
+
declare const NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
37
|
+
/** Safe to show a visitor. Only validation and `wrong_app` use the server's own text. */
|
|
37
38
|
declare function userMessage(error: FewchursError): string;
|
|
38
39
|
declare function networkError(message?: string): FewchursError;
|
|
39
40
|
declare function validationError(message: string): FewchursError;
|
|
@@ -143,9 +144,9 @@ interface FewchursClient {
|
|
|
143
144
|
}
|
|
144
145
|
declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
|
|
145
146
|
|
|
146
|
-
declare const SDK_VERSION = "1.
|
|
147
|
+
declare const SDK_VERSION = "1.5.0";
|
|
147
148
|
|
|
148
|
-
declare const SDK_HEADER = "react/1.
|
|
149
|
+
declare const SDK_HEADER = "react/1.5.0";
|
|
149
150
|
declare const DEFAULT_BASE_URL = "https://www.fewchurs.com/api/v1";
|
|
150
151
|
declare const DEFAULT_PAGE_SIZE = 20;
|
|
151
152
|
/** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
|
|
@@ -215,4 +216,4 @@ declare function validateFeatureInput(input: SubmitFeatureInput): FewchursError
|
|
|
215
216
|
declare function validateCommentInput(input: CreateCommentInput): FewchursError | null;
|
|
216
217
|
declare function validateEmail(email: string): FewchursError | null;
|
|
217
218
|
|
|
218
|
-
export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };
|
|
219
|
+
export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, NOT_CONFIGURED_MESSAGE, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };
|
package/dist/core.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(core_exports, {
|
|
|
23
23
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
24
|
DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
|
|
25
25
|
LIMITS: () => LIMITS,
|
|
26
|
+
NOT_CONFIGURED_MESSAGE: () => NOT_CONFIGURED_MESSAGE,
|
|
26
27
|
ROADMAP_STATUSES: () => ROADMAP_STATUSES,
|
|
27
28
|
SDK_HEADER: () => SDK_HEADER,
|
|
28
29
|
SDK_VERSION: () => SDK_VERSION,
|
|
@@ -48,7 +49,7 @@ __export(core_exports, {
|
|
|
48
49
|
module.exports = __toCommonJS(core_exports);
|
|
49
50
|
|
|
50
51
|
// src/client/version.ts
|
|
51
|
-
var SDK_VERSION = "1.
|
|
52
|
+
var SDK_VERSION = "1.5.0";
|
|
52
53
|
|
|
53
54
|
// src/client/constants.ts
|
|
54
55
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -66,17 +67,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
66
67
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
67
68
|
|
|
68
69
|
// src/client/errors.ts
|
|
70
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
69
71
|
var USER_MESSAGES = {
|
|
70
|
-
invalid_key: "
|
|
71
|
-
validation_failed: "
|
|
72
|
-
rate_limited: "You
|
|
73
|
-
not_found: "
|
|
74
|
-
limit_reached: "
|
|
75
|
-
server: "Something went wrong.
|
|
76
|
-
network: "
|
|
72
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
73
|
+
validation_failed: "Invalid input.",
|
|
74
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
75
|
+
not_found: "This request no longer exists.",
|
|
76
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
77
|
+
server: "Something went wrong. Try again.",
|
|
78
|
+
network: "You're offline."
|
|
77
79
|
};
|
|
80
|
+
function isWrongApp(error) {
|
|
81
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
82
|
+
}
|
|
83
|
+
function isNotConfigured(error) {
|
|
84
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
85
|
+
}
|
|
86
|
+
function notConfiguredError() {
|
|
87
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
88
|
+
}
|
|
78
89
|
function userMessage(error) {
|
|
79
90
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
91
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
80
92
|
return USER_MESSAGES[error.code];
|
|
81
93
|
}
|
|
82
94
|
function networkError(message = "Network request failed.") {
|
|
@@ -87,17 +99,17 @@ function validationError(message) {
|
|
|
87
99
|
}
|
|
88
100
|
function errorFromResponse(status, body) {
|
|
89
101
|
const code = body?.error?.code;
|
|
90
|
-
const
|
|
102
|
+
const serverMessage = body?.error?.message;
|
|
103
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
91
104
|
switch (code) {
|
|
92
105
|
case "invalid_key":
|
|
93
106
|
return { code: "invalid_key", status: 401, message };
|
|
94
107
|
case "wrong_app":
|
|
95
108
|
return { code: "invalid_key", status: 403, message };
|
|
96
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
97
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
98
109
|
case "missing_device_id":
|
|
110
|
+
return { code: "server", status: 400, message };
|
|
99
111
|
case "validation_failed":
|
|
100
|
-
return { code: "validation_failed", status: 400, message };
|
|
112
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
101
113
|
case "rate_limited":
|
|
102
114
|
return { code: "rate_limited", status: 429, message };
|
|
103
115
|
case "not_found":
|
|
@@ -191,11 +203,8 @@ function normalizeVote(value) {
|
|
|
191
203
|
// src/client/validate.ts
|
|
192
204
|
function validateFeatureInput(input) {
|
|
193
205
|
const title = input.title.trim();
|
|
194
|
-
if (title.length < LIMITS.titleMin) {
|
|
195
|
-
return validationError(`Title must be
|
|
196
|
-
}
|
|
197
|
-
if (title.length > LIMITS.titleMax) {
|
|
198
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
206
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
207
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
199
208
|
}
|
|
200
209
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
201
210
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -207,9 +216,8 @@ function validateFeatureInput(input) {
|
|
|
207
216
|
}
|
|
208
217
|
function validateCommentInput(input) {
|
|
209
218
|
const text = input.text.trim();
|
|
210
|
-
if (text.length < LIMITS.commentMin
|
|
211
|
-
|
|
212
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
219
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
220
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
213
221
|
}
|
|
214
222
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
215
223
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -229,6 +237,7 @@ function createFewchursClient(options = {}) {
|
|
|
229
237
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
230
238
|
const retries = options.retries ?? 1;
|
|
231
239
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
240
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
232
241
|
function getDeviceId() {
|
|
233
242
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
234
243
|
}
|
|
@@ -255,6 +264,11 @@ function createFewchursClient(options = {}) {
|
|
|
255
264
|
}
|
|
256
265
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
257
266
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
267
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
268
|
+
const failure = notConfiguredError();
|
|
269
|
+
logError(failure);
|
|
270
|
+
return { ok: false, error: failure };
|
|
271
|
+
}
|
|
258
272
|
const target = url(path, query);
|
|
259
273
|
const init = { method, headers: headers(body !== void 0) };
|
|
260
274
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -270,7 +284,9 @@ function createFewchursClient(options = {}) {
|
|
|
270
284
|
await delay(retryDelayMs);
|
|
271
285
|
continue;
|
|
272
286
|
}
|
|
273
|
-
|
|
287
|
+
const failure2 = networkError(messageOf(error));
|
|
288
|
+
logError(failure2);
|
|
289
|
+
return { ok: false, error: failure2 };
|
|
274
290
|
}
|
|
275
291
|
if (response.ok) {
|
|
276
292
|
const payload = await readJson(response);
|
|
@@ -281,7 +297,7 @@ function createFewchursClient(options = {}) {
|
|
|
281
297
|
continue;
|
|
282
298
|
}
|
|
283
299
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
284
|
-
|
|
300
|
+
logError(failure);
|
|
285
301
|
return { ok: false, error: failure };
|
|
286
302
|
}
|
|
287
303
|
return { ok: false, error: networkError() };
|
|
@@ -372,14 +388,20 @@ function delay(ms) {
|
|
|
372
388
|
function messageOf(error) {
|
|
373
389
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
374
390
|
}
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
392
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
393
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
394
|
+
var loggedErrors = /* @__PURE__ */ new Set();
|
|
395
|
+
function logOnce(detail) {
|
|
378
396
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
if (loggedErrors.has(detail)) return;
|
|
398
|
+
loggedErrors.add(detail);
|
|
399
|
+
console.warn(`[fewchurs] : ${detail}`);
|
|
400
|
+
}
|
|
401
|
+
function logError(error) {
|
|
402
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
403
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
404
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
// src/identity/deviceId.ts
|
|
@@ -403,6 +425,7 @@ function featureKey(featureId) {
|
|
|
403
425
|
DEFAULT_BASE_URL,
|
|
404
426
|
DEFAULT_PAGE_SIZE,
|
|
405
427
|
LIMITS,
|
|
428
|
+
NOT_CONFIGURED_MESSAGE,
|
|
406
429
|
ROADMAP_STATUSES,
|
|
407
430
|
SDK_HEADER,
|
|
408
431
|
SDK_VERSION,
|
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.3.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.warn(\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":[]}
|
|
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 {\n errorFromResponse,\n networkError,\n NOT_CONFIGURED_MESSAGE,\n userMessage,\n validationError,\n} 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.5.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\nexport const NOT_CONFIGURED_MESSAGE = \"Fewchurs is not configured.\";\n\nconst USER_MESSAGES: Record<FewchursError[\"code\"], string> = {\n invalid_key: \"Invalid API key. Check the key in your Fewchurs dashboard.\",\n validation_failed: \"Invalid input.\",\n rate_limited: \"You've reached today's limit. Try again tomorrow.\",\n not_found: \"This request no longer exists.\",\n limit_reached: \"This board isn't accepting requests right now.\",\n server: \"Something went wrong. Try again.\",\n network: \"You're offline.\",\n};\n\nexport function isWrongApp(error: FewchursError): boolean {\n return error.code === \"invalid_key\" && error.status === 403;\n}\n\nexport function isNotConfigured(error: FewchursError): boolean {\n return error.code === \"server\" && error.message === NOT_CONFIGURED_MESSAGE;\n}\n\nexport function notConfiguredError(): FewchursError {\n return { code: \"server\", status: 500, message: NOT_CONFIGURED_MESSAGE };\n}\n\n/** Safe to show a visitor. Only validation and `wrong_app` use the server's own text. */\nexport function userMessage(error: FewchursError): string {\n if (error.code === \"validation_failed\" && error.message) return error.message;\n if (isWrongApp(error) && 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 serverMessage = (body as ServerErrorBody | null)?.error?.message;\n const message = serverMessage ?? `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 case \"missing_device_id\":\n return { code: \"server\", status: 400, message };\n case \"validation_failed\":\n return { code: \"validation_failed\", status: 400, message: serverMessage ?? \"Invalid input.\" };\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 || title.length > LIMITS.titleMax) {\n return validationError(`Title must be ${LIMITS.titleMin}–${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 || text.length > LIMITS.commentMax) {\n return validationError(`Comment must be ${LIMITS.commentMin}–${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 {\n errorFromResponse,\n isNotConfigured,\n networkError,\n notConfiguredError,\n type FewchursError,\n type Result,\n} 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 if (options.apiKey && !options.apiKey.startsWith(\"fr_\")) logOnce(BAD_KEY_PREFIX);\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 if (!options.apiKey && options.baseUrl === undefined) {\n const failure = notConfiguredError();\n logError(failure);\n return { ok: false, error: failure };\n }\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 const failure = networkError(messageOf(error));\n logError(failure);\n return { ok: false, error: failure };\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 logError(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\nconst INVALID_KEY_HELP =\n \"Fewchurs rejected the API key. Check the key in your dashboard (Apps → API keys).\";\n\nconst NOT_CONFIGURED_HELP =\n \"Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.\";\n\nconst BAD_KEY_PREFIX = 'The Fewchurs API key should start with \"fr_\". Copy it from your dashboard.';\n\nconst loggedErrors = new Set<string>();\n\nfunction logOnce(detail: string): void {\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") return;\n if (loggedErrors.has(detail)) return;\n loggedErrors.add(detail);\n console.warn(`[fewchurs] : ${detail}`);\n}\n\nfunction logError(error: FewchursError): void {\n if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);\n const isInvalidKey = error.code === \"invalid_key\" && error.status === 401;\n logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);\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":["failure"]}
|
package/dist/core.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client/version.ts
|
|
2
|
-
var SDK_VERSION = "1.
|
|
2
|
+
var SDK_VERSION = "1.5.0";
|
|
3
3
|
|
|
4
4
|
// src/client/constants.ts
|
|
5
5
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -17,17 +17,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
17
17
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
18
18
|
|
|
19
19
|
// src/client/errors.ts
|
|
20
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
20
21
|
var USER_MESSAGES = {
|
|
21
|
-
invalid_key: "
|
|
22
|
-
validation_failed: "
|
|
23
|
-
rate_limited: "You
|
|
24
|
-
not_found: "
|
|
25
|
-
limit_reached: "
|
|
26
|
-
server: "Something went wrong.
|
|
27
|
-
network: "
|
|
22
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
23
|
+
validation_failed: "Invalid input.",
|
|
24
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
25
|
+
not_found: "This request no longer exists.",
|
|
26
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
27
|
+
server: "Something went wrong. Try again.",
|
|
28
|
+
network: "You're offline."
|
|
28
29
|
};
|
|
30
|
+
function isWrongApp(error) {
|
|
31
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
32
|
+
}
|
|
33
|
+
function isNotConfigured(error) {
|
|
34
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
35
|
+
}
|
|
36
|
+
function notConfiguredError() {
|
|
37
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
38
|
+
}
|
|
29
39
|
function userMessage(error) {
|
|
30
40
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
41
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
31
42
|
return USER_MESSAGES[error.code];
|
|
32
43
|
}
|
|
33
44
|
function networkError(message = "Network request failed.") {
|
|
@@ -38,17 +49,17 @@ function validationError(message) {
|
|
|
38
49
|
}
|
|
39
50
|
function errorFromResponse(status, body) {
|
|
40
51
|
const code = body?.error?.code;
|
|
41
|
-
const
|
|
52
|
+
const serverMessage = body?.error?.message;
|
|
53
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
42
54
|
switch (code) {
|
|
43
55
|
case "invalid_key":
|
|
44
56
|
return { code: "invalid_key", status: 401, message };
|
|
45
57
|
case "wrong_app":
|
|
46
58
|
return { code: "invalid_key", status: 403, message };
|
|
47
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
48
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
49
59
|
case "missing_device_id":
|
|
60
|
+
return { code: "server", status: 400, message };
|
|
50
61
|
case "validation_failed":
|
|
51
|
-
return { code: "validation_failed", status: 400, message };
|
|
62
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
52
63
|
case "rate_limited":
|
|
53
64
|
return { code: "rate_limited", status: 429, message };
|
|
54
65
|
case "not_found":
|
|
@@ -142,11 +153,8 @@ function normalizeVote(value) {
|
|
|
142
153
|
// src/client/validate.ts
|
|
143
154
|
function validateFeatureInput(input) {
|
|
144
155
|
const title = input.title.trim();
|
|
145
|
-
if (title.length < LIMITS.titleMin) {
|
|
146
|
-
return validationError(`Title must be
|
|
147
|
-
}
|
|
148
|
-
if (title.length > LIMITS.titleMax) {
|
|
149
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
156
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
157
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
150
158
|
}
|
|
151
159
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
152
160
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -158,9 +166,8 @@ function validateFeatureInput(input) {
|
|
|
158
166
|
}
|
|
159
167
|
function validateCommentInput(input) {
|
|
160
168
|
const text = input.text.trim();
|
|
161
|
-
if (text.length < LIMITS.commentMin
|
|
162
|
-
|
|
163
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
169
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
170
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
164
171
|
}
|
|
165
172
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
166
173
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -180,6 +187,7 @@ function createFewchursClient(options = {}) {
|
|
|
180
187
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
181
188
|
const retries = options.retries ?? 1;
|
|
182
189
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
190
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
183
191
|
function getDeviceId() {
|
|
184
192
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
185
193
|
}
|
|
@@ -206,6 +214,11 @@ function createFewchursClient(options = {}) {
|
|
|
206
214
|
}
|
|
207
215
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
208
216
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
217
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
218
|
+
const failure = notConfiguredError();
|
|
219
|
+
logError(failure);
|
|
220
|
+
return { ok: false, error: failure };
|
|
221
|
+
}
|
|
209
222
|
const target = url(path, query);
|
|
210
223
|
const init = { method, headers: headers(body !== void 0) };
|
|
211
224
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -221,7 +234,9 @@ function createFewchursClient(options = {}) {
|
|
|
221
234
|
await delay(retryDelayMs);
|
|
222
235
|
continue;
|
|
223
236
|
}
|
|
224
|
-
|
|
237
|
+
const failure2 = networkError(messageOf(error));
|
|
238
|
+
logError(failure2);
|
|
239
|
+
return { ok: false, error: failure2 };
|
|
225
240
|
}
|
|
226
241
|
if (response.ok) {
|
|
227
242
|
const payload = await readJson(response);
|
|
@@ -232,7 +247,7 @@ function createFewchursClient(options = {}) {
|
|
|
232
247
|
continue;
|
|
233
248
|
}
|
|
234
249
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
235
|
-
|
|
250
|
+
logError(failure);
|
|
236
251
|
return { ok: false, error: failure };
|
|
237
252
|
}
|
|
238
253
|
return { ok: false, error: networkError() };
|
|
@@ -323,14 +338,20 @@ function delay(ms) {
|
|
|
323
338
|
function messageOf(error) {
|
|
324
339
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
325
340
|
}
|
|
326
|
-
var
|
|
327
|
-
|
|
328
|
-
|
|
341
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
342
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
343
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
344
|
+
var loggedErrors = /* @__PURE__ */ new Set();
|
|
345
|
+
function logOnce(detail) {
|
|
329
346
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
347
|
+
if (loggedErrors.has(detail)) return;
|
|
348
|
+
loggedErrors.add(detail);
|
|
349
|
+
console.warn(`[fewchurs] : ${detail}`);
|
|
350
|
+
}
|
|
351
|
+
function logError(error) {
|
|
352
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
353
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
354
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
334
355
|
}
|
|
335
356
|
|
|
336
357
|
// src/identity/deviceId.ts
|
|
@@ -353,6 +374,7 @@ export {
|
|
|
353
374
|
DEFAULT_BASE_URL,
|
|
354
375
|
DEFAULT_PAGE_SIZE,
|
|
355
376
|
LIMITS,
|
|
377
|
+
NOT_CONFIGURED_MESSAGE,
|
|
356
378
|
ROADMAP_STATUSES,
|
|
357
379
|
SDK_HEADER,
|
|
358
380
|
SDK_VERSION,
|