fdbck-node 0.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/dist/index.mjs ADDED
@@ -0,0 +1,362 @@
1
+ // src/errors.ts
2
+ var FdbckError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "FdbckError";
6
+ }
7
+ };
8
+ var FdbckApiError = class extends FdbckError {
9
+ status;
10
+ code;
11
+ details;
12
+ constructor(status, code, message, details) {
13
+ super(message);
14
+ this.name = "FdbckApiError";
15
+ this.status = status;
16
+ this.code = code;
17
+ this.details = details;
18
+ }
19
+ };
20
+ var FdbckNetworkError = class extends FdbckError {
21
+ constructor(message, options) {
22
+ super(message);
23
+ this.name = "FdbckNetworkError";
24
+ if (options?.cause) {
25
+ this.cause = options.cause;
26
+ }
27
+ }
28
+ };
29
+
30
+ // src/utils.ts
31
+ function mapKeys(obj, fieldMap) {
32
+ const result = {};
33
+ for (const [key, value] of Object.entries(obj)) {
34
+ const mappedKey = fieldMap[key] ?? key;
35
+ result[mappedKey] = value;
36
+ }
37
+ return result;
38
+ }
39
+ var questionFieldsToApi = {
40
+ ratingConfig: "rating_config",
41
+ expiresAt: "expires_at",
42
+ maxResponses: "max_responses",
43
+ webhookUrl: "webhook_url",
44
+ webhookTrigger: "webhook_trigger",
45
+ themeColor: "theme_color",
46
+ themeMode: "theme_mode",
47
+ hideBranding: "hide_branding",
48
+ welcomeMessage: "welcome_message",
49
+ thankYouMessage: "thank_you_message"
50
+ };
51
+ var questionFieldsFromApi = {
52
+ expires_at: "expiresAt",
53
+ max_responses: "maxResponses",
54
+ webhook_url: "webhookUrl",
55
+ webhook_trigger: "webhookTrigger",
56
+ webhook_secret: "webhookSecret",
57
+ theme_color: "themeColor",
58
+ theme_mode: "themeMode",
59
+ hide_branding: "hideBranding",
60
+ welcome_message: "welcomeMessage",
61
+ thank_you_message: "thankYouMessage",
62
+ total_responses: "totalResponses",
63
+ created_at: "createdAt",
64
+ updated_at: "updatedAt"
65
+ };
66
+ var ratingConfigFieldsToApi = {
67
+ minLabel: "min_label",
68
+ maxLabel: "max_label"
69
+ };
70
+ var ratingConfigFieldsFromApi = {
71
+ min_label: "minLabel",
72
+ max_label: "maxLabel"
73
+ };
74
+ var responseFieldsFromApi = {
75
+ question_id: "questionId",
76
+ created_at: "createdAt"
77
+ };
78
+ var tokenFieldsFromApi = {
79
+ respond_url: "respondUrl",
80
+ expires_at: "expiresAt"
81
+ };
82
+ var webhookDeliveryFieldsFromApi = {
83
+ status_code: "statusCode",
84
+ next_retry_at: "nextRetryAt",
85
+ created_at: "createdAt"
86
+ };
87
+ var paginationFieldsFromApi = {
88
+ next_cursor: "nextCursor",
89
+ has_more: "hasMore"
90
+ };
91
+ var accountOrgFieldsFromApi = {
92
+ responses_used: "responsesUsed",
93
+ responses_limit: "responsesLimit",
94
+ period_starts_at: "periodStartsAt",
95
+ period_ends_at: "periodEndsAt",
96
+ consecutive_overage_months: "consecutiveOverageMonths",
97
+ has_billing: "hasBilling"
98
+ };
99
+ var accountUserFieldsFromApi = {
100
+ avatar_url: "avatarUrl"
101
+ };
102
+ var resultsFieldsFromApi = {
103
+ question_id: "questionId",
104
+ total_responses: "totalResponses"
105
+ };
106
+ function computeExpiresAt(seconds) {
107
+ return new Date(Date.now() + seconds * 1e3).toISOString();
108
+ }
109
+
110
+ // src/resources/questions.ts
111
+ function mapQuestionFromApi(raw) {
112
+ const mapped = mapKeys(raw, questionFieldsFromApi);
113
+ if (mapped.ratingConfig && typeof mapped.ratingConfig === "object") {
114
+ mapped.ratingConfig = mapKeys(
115
+ mapped.ratingConfig,
116
+ ratingConfigFieldsFromApi
117
+ );
118
+ }
119
+ return mapped;
120
+ }
121
+ function mapResponseFromApi(raw) {
122
+ return mapKeys(raw, responseFieldsFromApi);
123
+ }
124
+ function mapWebhookDeliveryFromApi(raw) {
125
+ return mapKeys(raw, webhookDeliveryFieldsFromApi);
126
+ }
127
+ function mapPagination(raw) {
128
+ return mapKeys(raw, paginationFieldsFromApi);
129
+ }
130
+ var QuestionsResource = class {
131
+ constructor(client) {
132
+ this.client = client;
133
+ }
134
+ /** Create a new question. */
135
+ async create(opts) {
136
+ const { expiresIn, ratingConfig, ...rest } = opts;
137
+ if (expiresIn !== void 0 && opts.expiresAt !== void 0) {
138
+ throw new Error("Provide either expiresIn or expiresAt, not both");
139
+ }
140
+ if (expiresIn === void 0 && opts.expiresAt === void 0) {
141
+ throw new Error("Either expiresIn or expiresAt is required");
142
+ }
143
+ const body = { ...rest };
144
+ if (expiresIn !== void 0) {
145
+ body.expiresAt = computeExpiresAt(expiresIn);
146
+ }
147
+ if (ratingConfig) {
148
+ body.ratingConfig = mapKeys(
149
+ ratingConfig,
150
+ ratingConfigFieldsToApi
151
+ );
152
+ }
153
+ const apiBody = mapKeys(body, questionFieldsToApi);
154
+ const raw = await this.client.request("POST", "/v1/questions", {
155
+ body: apiBody
156
+ });
157
+ return mapQuestionFromApi(raw);
158
+ }
159
+ /** Get a question by ID. */
160
+ async get(id) {
161
+ const raw = await this.client.request("GET", `/v1/questions/${id}`);
162
+ return mapQuestionFromApi(raw);
163
+ }
164
+ /** List questions with pagination. */
165
+ async list(opts) {
166
+ const query = {};
167
+ if (opts?.cursor) query.cursor = opts.cursor;
168
+ if (opts?.limit) query.limit = String(opts.limit);
169
+ if (opts?.status) query.status = opts.status;
170
+ if (opts?.sort) query.sort = opts.sort;
171
+ if (opts?.order) query.order = opts.order;
172
+ if (opts?.createdAfter) query.created_after = opts.createdAfter;
173
+ if (opts?.createdBefore) query.created_before = opts.createdBefore;
174
+ const raw = await this.client.request("GET", "/v1/questions", {
175
+ query
176
+ });
177
+ const data = raw.data.map(mapQuestionFromApi);
178
+ const pagination = mapPagination(raw.pagination);
179
+ return { data, pagination };
180
+ }
181
+ /** Auto-paginate through all questions. */
182
+ async *listAll(opts) {
183
+ let cursor;
184
+ do {
185
+ const page = await this.list({ ...opts, cursor });
186
+ for (const question of page.data) {
187
+ yield question;
188
+ }
189
+ cursor = page.pagination.nextCursor ?? void 0;
190
+ } while (cursor);
191
+ }
192
+ /** Get responses for a question with aggregated results. */
193
+ async results(id, opts) {
194
+ const query = {};
195
+ if (opts?.cursor) query.cursor = opts.cursor;
196
+ if (opts?.limit) query.limit = String(opts.limit);
197
+ const raw = await this.client.request(
198
+ "GET",
199
+ `/v1/questions/${id}/responses`,
200
+ { query }
201
+ );
202
+ const envelope = mapKeys(raw, resultsFieldsFromApi);
203
+ const data = raw.data.map(mapResponseFromApi);
204
+ const pagination = mapPagination(raw.pagination);
205
+ return {
206
+ questionId: envelope.questionId,
207
+ type: envelope.type,
208
+ status: envelope.status,
209
+ totalResponses: envelope.totalResponses,
210
+ results: envelope.results,
211
+ data,
212
+ pagination
213
+ };
214
+ }
215
+ /** Cancel (delete) a question. */
216
+ async cancel(id) {
217
+ const raw = await this.client.request("DELETE", `/v1/questions/${id}`);
218
+ return mapQuestionFromApi(raw);
219
+ }
220
+ /** Get webhook delivery logs for a question. */
221
+ async webhooks(id, opts) {
222
+ const query = {};
223
+ if (opts?.cursor) query.cursor = opts.cursor;
224
+ if (opts?.limit) query.limit = String(opts.limit);
225
+ const raw = await this.client.request(
226
+ "GET",
227
+ `/v1/questions/${id}/webhooks`,
228
+ { query }
229
+ );
230
+ const data = raw.data.map(mapWebhookDeliveryFromApi);
231
+ const pagination = mapPagination(raw.pagination);
232
+ return { data, pagination };
233
+ }
234
+ };
235
+
236
+ // src/resources/tokens.ts
237
+ var TokensResource = class {
238
+ constructor(client) {
239
+ this.client = client;
240
+ }
241
+ /** Create a respondent token for a question. */
242
+ async create(questionId, opts) {
243
+ const raw = await this.client.request(
244
+ "POST",
245
+ `/v1/questions/${questionId}/token`,
246
+ { body: opts ?? {} }
247
+ );
248
+ return mapKeys(raw, tokenFieldsFromApi);
249
+ }
250
+ };
251
+
252
+ // src/webhook.ts
253
+ import { createHmac, timingSafeEqual } from "crypto";
254
+ function verifyWebhook(rawBody, signature, secret) {
255
+ const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
256
+ if (expected.length !== signature.length) {
257
+ return false;
258
+ }
259
+ return timingSafeEqual(
260
+ Buffer.from(expected, "hex"),
261
+ Buffer.from(signature, "hex")
262
+ );
263
+ }
264
+
265
+ // src/client.ts
266
+ var DEFAULT_BASE_URL = "https://api.fdbck.sh";
267
+ var DEFAULT_TIMEOUT = 3e4;
268
+ var SDK_VERSION = "0.1.0";
269
+ var Fdbck = class {
270
+ apiKey;
271
+ baseUrl;
272
+ timeout;
273
+ questions;
274
+ tokens;
275
+ constructor(apiKey, options) {
276
+ if (!apiKey.startsWith("sk_fdbck_")) {
277
+ throw new Error('Invalid API key: must start with "sk_fdbck_"');
278
+ }
279
+ this.apiKey = apiKey;
280
+ this.baseUrl = (options?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
281
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
282
+ this.questions = new QuestionsResource(this);
283
+ this.tokens = new TokensResource(this);
284
+ }
285
+ /** Make an authenticated request to the fdbck API. */
286
+ async request(method, path, options) {
287
+ const url = new URL(path, this.baseUrl);
288
+ if (options?.query) {
289
+ for (const [key, value] of Object.entries(options.query)) {
290
+ url.searchParams.set(key, value);
291
+ }
292
+ }
293
+ const headers = {
294
+ Authorization: `Bearer ${this.apiKey}`,
295
+ "User-Agent": `@fdbck/node/${SDK_VERSION}`
296
+ };
297
+ const fetchOptions = {
298
+ method,
299
+ headers
300
+ };
301
+ if (options?.body) {
302
+ headers["Content-Type"] = "application/json";
303
+ fetchOptions.body = JSON.stringify(options.body);
304
+ }
305
+ const controller = new AbortController();
306
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
307
+ fetchOptions.signal = controller.signal;
308
+ let response;
309
+ try {
310
+ response = await fetch(url.toString(), fetchOptions);
311
+ } catch (error) {
312
+ clearTimeout(timeoutId);
313
+ throw new FdbckNetworkError(
314
+ error instanceof Error ? error.message : "Network request failed",
315
+ { cause: error }
316
+ );
317
+ } finally {
318
+ clearTimeout(timeoutId);
319
+ }
320
+ if (response.status === 204) {
321
+ return void 0;
322
+ }
323
+ let data;
324
+ try {
325
+ data = await response.json();
326
+ } catch {
327
+ throw new FdbckApiError(response.status, "parse_error", "Failed to parse response body");
328
+ }
329
+ if (!response.ok) {
330
+ const err = data?.error;
331
+ throw new FdbckApiError(
332
+ response.status,
333
+ err?.code ?? "unknown_error",
334
+ err?.message ?? `Request failed with status ${response.status}`,
335
+ err?.details
336
+ );
337
+ }
338
+ return data;
339
+ }
340
+ /** Get current account info. */
341
+ async me() {
342
+ const raw = await this.request("GET", "/v1/me");
343
+ const rawUser = raw.user;
344
+ const rawOrg = raw.organization;
345
+ return {
346
+ user: rawUser ? mapKeys(rawUser, accountUserFieldsFromApi) : null,
347
+ organization: rawOrg ? mapKeys(rawOrg, accountOrgFieldsFromApi) : null
348
+ };
349
+ }
350
+ /** Verify a webhook signature. */
351
+ verifyWebhook(rawBody, signature, secret) {
352
+ return verifyWebhook(rawBody, signature, secret);
353
+ }
354
+ };
355
+ export {
356
+ Fdbck,
357
+ FdbckApiError,
358
+ FdbckError,
359
+ FdbckNetworkError,
360
+ Fdbck as default,
361
+ verifyWebhook
362
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "fdbck-node",
3
+ "version": "0.1.0",
4
+ "description": "Official Node.js SDK for the fdbck API",
5
+ "license": "MIT",
6
+ "author": "fdbck",
7
+ "homepage": "https://fdbck.sh",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/fdbck-sh/fdbck-node"
11
+ },
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.5.0",
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^5.4.0",
43
+ "vitest": "^1.6.0"
44
+ }
45
+ }