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/README.md +315 -0
- package/dist/index.d.mts +235 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +393 -0
- package/dist/index.mjs +362 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Question type */
|
|
2
|
+
type QuestionType = 'yes_no' | 'single_choice' | 'multiple_choice' | 'rating';
|
|
3
|
+
/** Question status */
|
|
4
|
+
type QuestionStatus = 'collecting' | 'completed' | 'expired' | 'cancelled';
|
|
5
|
+
/** Webhook trigger */
|
|
6
|
+
type WebhookTrigger = 'expiry' | 'each_response' | 'both';
|
|
7
|
+
/** Rating scale configuration */
|
|
8
|
+
interface RatingConfig {
|
|
9
|
+
min: number;
|
|
10
|
+
max: number;
|
|
11
|
+
minLabel?: string;
|
|
12
|
+
maxLabel?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Options for creating a question */
|
|
15
|
+
interface CreateQuestionOptions {
|
|
16
|
+
question: string;
|
|
17
|
+
type: QuestionType;
|
|
18
|
+
options?: string[];
|
|
19
|
+
ratingConfig?: RatingConfig;
|
|
20
|
+
/** Seconds until expiry — mutually exclusive with `expiresAt` */
|
|
21
|
+
expiresIn?: number;
|
|
22
|
+
/** ISO date string — mutually exclusive with `expiresIn` */
|
|
23
|
+
expiresAt?: string;
|
|
24
|
+
maxResponses?: number;
|
|
25
|
+
webhookUrl?: string;
|
|
26
|
+
webhookTrigger?: WebhookTrigger;
|
|
27
|
+
metadata?: Record<string, string>;
|
|
28
|
+
themeColor?: string;
|
|
29
|
+
themeMode?: 'light' | 'dark';
|
|
30
|
+
hideBranding?: boolean;
|
|
31
|
+
welcomeMessage?: string;
|
|
32
|
+
thankYouMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A question resource */
|
|
35
|
+
interface Question {
|
|
36
|
+
id: string;
|
|
37
|
+
question: string;
|
|
38
|
+
type: QuestionType;
|
|
39
|
+
options: string[] | null;
|
|
40
|
+
ratingConfig?: RatingConfig;
|
|
41
|
+
status: QuestionStatus;
|
|
42
|
+
expiresAt: string;
|
|
43
|
+
maxResponses: number | null;
|
|
44
|
+
webhookUrl?: string;
|
|
45
|
+
webhookTrigger?: WebhookTrigger;
|
|
46
|
+
webhookSecret?: string;
|
|
47
|
+
metadata: Record<string, string> | null;
|
|
48
|
+
themeColor: string;
|
|
49
|
+
themeMode: string;
|
|
50
|
+
hideBranding: boolean;
|
|
51
|
+
welcomeMessage: string | null;
|
|
52
|
+
thankYouMessage: string | null;
|
|
53
|
+
totalResponses?: number;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
}
|
|
57
|
+
/** Options for listing questions */
|
|
58
|
+
interface ListQuestionsOptions {
|
|
59
|
+
cursor?: string;
|
|
60
|
+
limit?: number;
|
|
61
|
+
status?: QuestionStatus;
|
|
62
|
+
sort?: 'created_at' | 'updated_at';
|
|
63
|
+
order?: 'asc' | 'desc';
|
|
64
|
+
createdAfter?: string;
|
|
65
|
+
createdBefore?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Paginated list envelope */
|
|
68
|
+
interface PaginatedList<T> {
|
|
69
|
+
data: T[];
|
|
70
|
+
pagination: {
|
|
71
|
+
nextCursor: string | null;
|
|
72
|
+
hasMore: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** A single response item */
|
|
76
|
+
interface ResponseItem {
|
|
77
|
+
id: string;
|
|
78
|
+
questionId: string;
|
|
79
|
+
value: unknown;
|
|
80
|
+
respondent: string | null;
|
|
81
|
+
createdAt: string;
|
|
82
|
+
}
|
|
83
|
+
/** Options for listing responses */
|
|
84
|
+
interface ListResponsesOptions {
|
|
85
|
+
cursor?: string;
|
|
86
|
+
limit?: number;
|
|
87
|
+
}
|
|
88
|
+
/** A webhook delivery log entry */
|
|
89
|
+
interface WebhookDelivery {
|
|
90
|
+
id: string;
|
|
91
|
+
event: string;
|
|
92
|
+
success: boolean;
|
|
93
|
+
statusCode: number | null;
|
|
94
|
+
attempts: number;
|
|
95
|
+
error: string | null;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
nextRetryAt: string | null;
|
|
98
|
+
}
|
|
99
|
+
/** Options for listing webhook deliveries */
|
|
100
|
+
interface ListWebhooksOptions {
|
|
101
|
+
cursor?: string;
|
|
102
|
+
limit?: number;
|
|
103
|
+
}
|
|
104
|
+
/** Options for creating a token */
|
|
105
|
+
interface CreateTokenOptions {
|
|
106
|
+
respondent?: string;
|
|
107
|
+
metadata?: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
/** Token creation result */
|
|
110
|
+
interface TokenResult {
|
|
111
|
+
token: string;
|
|
112
|
+
respondUrl: string;
|
|
113
|
+
expiresAt: string;
|
|
114
|
+
}
|
|
115
|
+
/** Account info from GET /v1/me */
|
|
116
|
+
interface AccountInfo {
|
|
117
|
+
user: {
|
|
118
|
+
id: string;
|
|
119
|
+
email: string;
|
|
120
|
+
name: string | null;
|
|
121
|
+
avatarUrl: string | null;
|
|
122
|
+
} | null;
|
|
123
|
+
organization: {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
slug: string;
|
|
127
|
+
plan: string;
|
|
128
|
+
role: string | null;
|
|
129
|
+
responsesUsed: number;
|
|
130
|
+
responsesLimit: number | null;
|
|
131
|
+
periodStartsAt: string;
|
|
132
|
+
periodEndsAt: string | null;
|
|
133
|
+
consecutiveOverageMonths: number;
|
|
134
|
+
hasBilling: boolean;
|
|
135
|
+
} | null;
|
|
136
|
+
}
|
|
137
|
+
/** Aggregated results with paginated individual responses */
|
|
138
|
+
interface QuestionResultsResponse {
|
|
139
|
+
questionId: string;
|
|
140
|
+
type: string;
|
|
141
|
+
status: string;
|
|
142
|
+
totalResponses: number;
|
|
143
|
+
results: Record<string, unknown>;
|
|
144
|
+
data: ResponseItem[];
|
|
145
|
+
pagination: {
|
|
146
|
+
nextCursor: string | null;
|
|
147
|
+
hasMore: boolean;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** SDK client options */
|
|
151
|
+
interface FdbckOptions {
|
|
152
|
+
/** Base URL for the API (default: https://api.fdbck.sh) */
|
|
153
|
+
baseUrl?: string;
|
|
154
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
155
|
+
timeout?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
declare class QuestionsResource {
|
|
159
|
+
private readonly client;
|
|
160
|
+
constructor(client: Fdbck);
|
|
161
|
+
/** Create a new question. */
|
|
162
|
+
create(opts: CreateQuestionOptions): Promise<Question>;
|
|
163
|
+
/** Get a question by ID. */
|
|
164
|
+
get(id: string): Promise<Question>;
|
|
165
|
+
/** List questions with pagination. */
|
|
166
|
+
list(opts?: ListQuestionsOptions): Promise<PaginatedList<Question>>;
|
|
167
|
+
/** Auto-paginate through all questions. */
|
|
168
|
+
listAll(opts?: Omit<ListQuestionsOptions, 'cursor'>): AsyncGenerator<Question>;
|
|
169
|
+
/** Get responses for a question with aggregated results. */
|
|
170
|
+
results(id: string, opts?: ListResponsesOptions): Promise<QuestionResultsResponse>;
|
|
171
|
+
/** Cancel (delete) a question. */
|
|
172
|
+
cancel(id: string): Promise<Question>;
|
|
173
|
+
/** Get webhook delivery logs for a question. */
|
|
174
|
+
webhooks(id: string, opts?: ListWebhooksOptions): Promise<PaginatedList<WebhookDelivery>>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class TokensResource {
|
|
178
|
+
private readonly client;
|
|
179
|
+
constructor(client: Fdbck);
|
|
180
|
+
/** Create a respondent token for a question. */
|
|
181
|
+
create(questionId: string, opts?: CreateTokenOptions): Promise<TokenResult>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface RequestOptions {
|
|
185
|
+
body?: Record<string, unknown>;
|
|
186
|
+
query?: Record<string, string>;
|
|
187
|
+
}
|
|
188
|
+
declare class Fdbck {
|
|
189
|
+
private readonly apiKey;
|
|
190
|
+
private readonly baseUrl;
|
|
191
|
+
private readonly timeout;
|
|
192
|
+
readonly questions: QuestionsResource;
|
|
193
|
+
readonly tokens: TokensResource;
|
|
194
|
+
constructor(apiKey: string, options?: FdbckOptions);
|
|
195
|
+
/** Make an authenticated request to the fdbck API. */
|
|
196
|
+
request<T = unknown>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
197
|
+
/** Get current account info. */
|
|
198
|
+
me(): Promise<AccountInfo>;
|
|
199
|
+
/** Verify a webhook signature. */
|
|
200
|
+
verifyWebhook(rawBody: string, signature: string, secret: string): boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Base error for all fdbck SDK errors */
|
|
204
|
+
declare class FdbckError extends Error {
|
|
205
|
+
constructor(message: string);
|
|
206
|
+
}
|
|
207
|
+
/** Error returned by the fdbck API */
|
|
208
|
+
declare class FdbckApiError extends FdbckError {
|
|
209
|
+
readonly status: number;
|
|
210
|
+
readonly code: string;
|
|
211
|
+
readonly details?: {
|
|
212
|
+
fields: string[];
|
|
213
|
+
};
|
|
214
|
+
constructor(status: number, code: string, message: string, details?: {
|
|
215
|
+
fields: string[];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Network-level error (fetch failure, timeout, etc.) */
|
|
219
|
+
declare class FdbckNetworkError extends FdbckError {
|
|
220
|
+
constructor(message: string, options?: {
|
|
221
|
+
cause?: unknown;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Verify an incoming webhook signature from fdbck.
|
|
227
|
+
*
|
|
228
|
+
* @param rawBody - The raw JSON string body of the webhook request
|
|
229
|
+
* @param signature - The value of the `X-FDBCK-Signature` header
|
|
230
|
+
* @param secret - The webhook secret from question creation
|
|
231
|
+
* @returns `true` if the signature is valid
|
|
232
|
+
*/
|
|
233
|
+
declare function verifyWebhook(rawBody: string, signature: string, secret: string): boolean;
|
|
234
|
+
|
|
235
|
+
export { type AccountInfo, type CreateQuestionOptions, type CreateTokenOptions, Fdbck, FdbckApiError, FdbckError, FdbckNetworkError, type FdbckOptions, type ListQuestionsOptions, type ListResponsesOptions, type ListWebhooksOptions, type PaginatedList, type Question, type QuestionResultsResponse, type QuestionStatus, type QuestionType, type RatingConfig, type ResponseItem, type TokenResult, type WebhookDelivery, type WebhookTrigger, Fdbck as default, verifyWebhook };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Fdbck: () => Fdbck,
|
|
24
|
+
FdbckApiError: () => FdbckApiError,
|
|
25
|
+
FdbckError: () => FdbckError,
|
|
26
|
+
FdbckNetworkError: () => FdbckNetworkError,
|
|
27
|
+
default: () => Fdbck,
|
|
28
|
+
verifyWebhook: () => verifyWebhook
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/errors.ts
|
|
33
|
+
var FdbckError = class extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "FdbckError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var FdbckApiError = class extends FdbckError {
|
|
40
|
+
status;
|
|
41
|
+
code;
|
|
42
|
+
details;
|
|
43
|
+
constructor(status, code, message, details) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "FdbckApiError";
|
|
46
|
+
this.status = status;
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.details = details;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var FdbckNetworkError = class extends FdbckError {
|
|
52
|
+
constructor(message, options) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "FdbckNetworkError";
|
|
55
|
+
if (options?.cause) {
|
|
56
|
+
this.cause = options.cause;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/utils.ts
|
|
62
|
+
function mapKeys(obj, fieldMap) {
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
65
|
+
const mappedKey = fieldMap[key] ?? key;
|
|
66
|
+
result[mappedKey] = value;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
var questionFieldsToApi = {
|
|
71
|
+
ratingConfig: "rating_config",
|
|
72
|
+
expiresAt: "expires_at",
|
|
73
|
+
maxResponses: "max_responses",
|
|
74
|
+
webhookUrl: "webhook_url",
|
|
75
|
+
webhookTrigger: "webhook_trigger",
|
|
76
|
+
themeColor: "theme_color",
|
|
77
|
+
themeMode: "theme_mode",
|
|
78
|
+
hideBranding: "hide_branding",
|
|
79
|
+
welcomeMessage: "welcome_message",
|
|
80
|
+
thankYouMessage: "thank_you_message"
|
|
81
|
+
};
|
|
82
|
+
var questionFieldsFromApi = {
|
|
83
|
+
expires_at: "expiresAt",
|
|
84
|
+
max_responses: "maxResponses",
|
|
85
|
+
webhook_url: "webhookUrl",
|
|
86
|
+
webhook_trigger: "webhookTrigger",
|
|
87
|
+
webhook_secret: "webhookSecret",
|
|
88
|
+
theme_color: "themeColor",
|
|
89
|
+
theme_mode: "themeMode",
|
|
90
|
+
hide_branding: "hideBranding",
|
|
91
|
+
welcome_message: "welcomeMessage",
|
|
92
|
+
thank_you_message: "thankYouMessage",
|
|
93
|
+
total_responses: "totalResponses",
|
|
94
|
+
created_at: "createdAt",
|
|
95
|
+
updated_at: "updatedAt"
|
|
96
|
+
};
|
|
97
|
+
var ratingConfigFieldsToApi = {
|
|
98
|
+
minLabel: "min_label",
|
|
99
|
+
maxLabel: "max_label"
|
|
100
|
+
};
|
|
101
|
+
var ratingConfigFieldsFromApi = {
|
|
102
|
+
min_label: "minLabel",
|
|
103
|
+
max_label: "maxLabel"
|
|
104
|
+
};
|
|
105
|
+
var responseFieldsFromApi = {
|
|
106
|
+
question_id: "questionId",
|
|
107
|
+
created_at: "createdAt"
|
|
108
|
+
};
|
|
109
|
+
var tokenFieldsFromApi = {
|
|
110
|
+
respond_url: "respondUrl",
|
|
111
|
+
expires_at: "expiresAt"
|
|
112
|
+
};
|
|
113
|
+
var webhookDeliveryFieldsFromApi = {
|
|
114
|
+
status_code: "statusCode",
|
|
115
|
+
next_retry_at: "nextRetryAt",
|
|
116
|
+
created_at: "createdAt"
|
|
117
|
+
};
|
|
118
|
+
var paginationFieldsFromApi = {
|
|
119
|
+
next_cursor: "nextCursor",
|
|
120
|
+
has_more: "hasMore"
|
|
121
|
+
};
|
|
122
|
+
var accountOrgFieldsFromApi = {
|
|
123
|
+
responses_used: "responsesUsed",
|
|
124
|
+
responses_limit: "responsesLimit",
|
|
125
|
+
period_starts_at: "periodStartsAt",
|
|
126
|
+
period_ends_at: "periodEndsAt",
|
|
127
|
+
consecutive_overage_months: "consecutiveOverageMonths",
|
|
128
|
+
has_billing: "hasBilling"
|
|
129
|
+
};
|
|
130
|
+
var accountUserFieldsFromApi = {
|
|
131
|
+
avatar_url: "avatarUrl"
|
|
132
|
+
};
|
|
133
|
+
var resultsFieldsFromApi = {
|
|
134
|
+
question_id: "questionId",
|
|
135
|
+
total_responses: "totalResponses"
|
|
136
|
+
};
|
|
137
|
+
function computeExpiresAt(seconds) {
|
|
138
|
+
return new Date(Date.now() + seconds * 1e3).toISOString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/resources/questions.ts
|
|
142
|
+
function mapQuestionFromApi(raw) {
|
|
143
|
+
const mapped = mapKeys(raw, questionFieldsFromApi);
|
|
144
|
+
if (mapped.ratingConfig && typeof mapped.ratingConfig === "object") {
|
|
145
|
+
mapped.ratingConfig = mapKeys(
|
|
146
|
+
mapped.ratingConfig,
|
|
147
|
+
ratingConfigFieldsFromApi
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return mapped;
|
|
151
|
+
}
|
|
152
|
+
function mapResponseFromApi(raw) {
|
|
153
|
+
return mapKeys(raw, responseFieldsFromApi);
|
|
154
|
+
}
|
|
155
|
+
function mapWebhookDeliveryFromApi(raw) {
|
|
156
|
+
return mapKeys(raw, webhookDeliveryFieldsFromApi);
|
|
157
|
+
}
|
|
158
|
+
function mapPagination(raw) {
|
|
159
|
+
return mapKeys(raw, paginationFieldsFromApi);
|
|
160
|
+
}
|
|
161
|
+
var QuestionsResource = class {
|
|
162
|
+
constructor(client) {
|
|
163
|
+
this.client = client;
|
|
164
|
+
}
|
|
165
|
+
/** Create a new question. */
|
|
166
|
+
async create(opts) {
|
|
167
|
+
const { expiresIn, ratingConfig, ...rest } = opts;
|
|
168
|
+
if (expiresIn !== void 0 && opts.expiresAt !== void 0) {
|
|
169
|
+
throw new Error("Provide either expiresIn or expiresAt, not both");
|
|
170
|
+
}
|
|
171
|
+
if (expiresIn === void 0 && opts.expiresAt === void 0) {
|
|
172
|
+
throw new Error("Either expiresIn or expiresAt is required");
|
|
173
|
+
}
|
|
174
|
+
const body = { ...rest };
|
|
175
|
+
if (expiresIn !== void 0) {
|
|
176
|
+
body.expiresAt = computeExpiresAt(expiresIn);
|
|
177
|
+
}
|
|
178
|
+
if (ratingConfig) {
|
|
179
|
+
body.ratingConfig = mapKeys(
|
|
180
|
+
ratingConfig,
|
|
181
|
+
ratingConfigFieldsToApi
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const apiBody = mapKeys(body, questionFieldsToApi);
|
|
185
|
+
const raw = await this.client.request("POST", "/v1/questions", {
|
|
186
|
+
body: apiBody
|
|
187
|
+
});
|
|
188
|
+
return mapQuestionFromApi(raw);
|
|
189
|
+
}
|
|
190
|
+
/** Get a question by ID. */
|
|
191
|
+
async get(id) {
|
|
192
|
+
const raw = await this.client.request("GET", `/v1/questions/${id}`);
|
|
193
|
+
return mapQuestionFromApi(raw);
|
|
194
|
+
}
|
|
195
|
+
/** List questions with pagination. */
|
|
196
|
+
async list(opts) {
|
|
197
|
+
const query = {};
|
|
198
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
199
|
+
if (opts?.limit) query.limit = String(opts.limit);
|
|
200
|
+
if (opts?.status) query.status = opts.status;
|
|
201
|
+
if (opts?.sort) query.sort = opts.sort;
|
|
202
|
+
if (opts?.order) query.order = opts.order;
|
|
203
|
+
if (opts?.createdAfter) query.created_after = opts.createdAfter;
|
|
204
|
+
if (opts?.createdBefore) query.created_before = opts.createdBefore;
|
|
205
|
+
const raw = await this.client.request("GET", "/v1/questions", {
|
|
206
|
+
query
|
|
207
|
+
});
|
|
208
|
+
const data = raw.data.map(mapQuestionFromApi);
|
|
209
|
+
const pagination = mapPagination(raw.pagination);
|
|
210
|
+
return { data, pagination };
|
|
211
|
+
}
|
|
212
|
+
/** Auto-paginate through all questions. */
|
|
213
|
+
async *listAll(opts) {
|
|
214
|
+
let cursor;
|
|
215
|
+
do {
|
|
216
|
+
const page = await this.list({ ...opts, cursor });
|
|
217
|
+
for (const question of page.data) {
|
|
218
|
+
yield question;
|
|
219
|
+
}
|
|
220
|
+
cursor = page.pagination.nextCursor ?? void 0;
|
|
221
|
+
} while (cursor);
|
|
222
|
+
}
|
|
223
|
+
/** Get responses for a question with aggregated results. */
|
|
224
|
+
async results(id, opts) {
|
|
225
|
+
const query = {};
|
|
226
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
227
|
+
if (opts?.limit) query.limit = String(opts.limit);
|
|
228
|
+
const raw = await this.client.request(
|
|
229
|
+
"GET",
|
|
230
|
+
`/v1/questions/${id}/responses`,
|
|
231
|
+
{ query }
|
|
232
|
+
);
|
|
233
|
+
const envelope = mapKeys(raw, resultsFieldsFromApi);
|
|
234
|
+
const data = raw.data.map(mapResponseFromApi);
|
|
235
|
+
const pagination = mapPagination(raw.pagination);
|
|
236
|
+
return {
|
|
237
|
+
questionId: envelope.questionId,
|
|
238
|
+
type: envelope.type,
|
|
239
|
+
status: envelope.status,
|
|
240
|
+
totalResponses: envelope.totalResponses,
|
|
241
|
+
results: envelope.results,
|
|
242
|
+
data,
|
|
243
|
+
pagination
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** Cancel (delete) a question. */
|
|
247
|
+
async cancel(id) {
|
|
248
|
+
const raw = await this.client.request("DELETE", `/v1/questions/${id}`);
|
|
249
|
+
return mapQuestionFromApi(raw);
|
|
250
|
+
}
|
|
251
|
+
/** Get webhook delivery logs for a question. */
|
|
252
|
+
async webhooks(id, opts) {
|
|
253
|
+
const query = {};
|
|
254
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
255
|
+
if (opts?.limit) query.limit = String(opts.limit);
|
|
256
|
+
const raw = await this.client.request(
|
|
257
|
+
"GET",
|
|
258
|
+
`/v1/questions/${id}/webhooks`,
|
|
259
|
+
{ query }
|
|
260
|
+
);
|
|
261
|
+
const data = raw.data.map(mapWebhookDeliveryFromApi);
|
|
262
|
+
const pagination = mapPagination(raw.pagination);
|
|
263
|
+
return { data, pagination };
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/resources/tokens.ts
|
|
268
|
+
var TokensResource = class {
|
|
269
|
+
constructor(client) {
|
|
270
|
+
this.client = client;
|
|
271
|
+
}
|
|
272
|
+
/** Create a respondent token for a question. */
|
|
273
|
+
async create(questionId, opts) {
|
|
274
|
+
const raw = await this.client.request(
|
|
275
|
+
"POST",
|
|
276
|
+
`/v1/questions/${questionId}/token`,
|
|
277
|
+
{ body: opts ?? {} }
|
|
278
|
+
);
|
|
279
|
+
return mapKeys(raw, tokenFieldsFromApi);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/webhook.ts
|
|
284
|
+
var import_node_crypto = require("crypto");
|
|
285
|
+
function verifyWebhook(rawBody, signature, secret) {
|
|
286
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody).digest("hex");
|
|
287
|
+
if (expected.length !== signature.length) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return (0, import_node_crypto.timingSafeEqual)(
|
|
291
|
+
Buffer.from(expected, "hex"),
|
|
292
|
+
Buffer.from(signature, "hex")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/client.ts
|
|
297
|
+
var DEFAULT_BASE_URL = "https://api.fdbck.sh";
|
|
298
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
299
|
+
var SDK_VERSION = "0.1.0";
|
|
300
|
+
var Fdbck = class {
|
|
301
|
+
apiKey;
|
|
302
|
+
baseUrl;
|
|
303
|
+
timeout;
|
|
304
|
+
questions;
|
|
305
|
+
tokens;
|
|
306
|
+
constructor(apiKey, options) {
|
|
307
|
+
if (!apiKey.startsWith("sk_fdbck_")) {
|
|
308
|
+
throw new Error('Invalid API key: must start with "sk_fdbck_"');
|
|
309
|
+
}
|
|
310
|
+
this.apiKey = apiKey;
|
|
311
|
+
this.baseUrl = (options?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
312
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
313
|
+
this.questions = new QuestionsResource(this);
|
|
314
|
+
this.tokens = new TokensResource(this);
|
|
315
|
+
}
|
|
316
|
+
/** Make an authenticated request to the fdbck API. */
|
|
317
|
+
async request(method, path, options) {
|
|
318
|
+
const url = new URL(path, this.baseUrl);
|
|
319
|
+
if (options?.query) {
|
|
320
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
321
|
+
url.searchParams.set(key, value);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const headers = {
|
|
325
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
326
|
+
"User-Agent": `@fdbck/node/${SDK_VERSION}`
|
|
327
|
+
};
|
|
328
|
+
const fetchOptions = {
|
|
329
|
+
method,
|
|
330
|
+
headers
|
|
331
|
+
};
|
|
332
|
+
if (options?.body) {
|
|
333
|
+
headers["Content-Type"] = "application/json";
|
|
334
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
335
|
+
}
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
338
|
+
fetchOptions.signal = controller.signal;
|
|
339
|
+
let response;
|
|
340
|
+
try {
|
|
341
|
+
response = await fetch(url.toString(), fetchOptions);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
clearTimeout(timeoutId);
|
|
344
|
+
throw new FdbckNetworkError(
|
|
345
|
+
error instanceof Error ? error.message : "Network request failed",
|
|
346
|
+
{ cause: error }
|
|
347
|
+
);
|
|
348
|
+
} finally {
|
|
349
|
+
clearTimeout(timeoutId);
|
|
350
|
+
}
|
|
351
|
+
if (response.status === 204) {
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
let data;
|
|
355
|
+
try {
|
|
356
|
+
data = await response.json();
|
|
357
|
+
} catch {
|
|
358
|
+
throw new FdbckApiError(response.status, "parse_error", "Failed to parse response body");
|
|
359
|
+
}
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
const err = data?.error;
|
|
362
|
+
throw new FdbckApiError(
|
|
363
|
+
response.status,
|
|
364
|
+
err?.code ?? "unknown_error",
|
|
365
|
+
err?.message ?? `Request failed with status ${response.status}`,
|
|
366
|
+
err?.details
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return data;
|
|
370
|
+
}
|
|
371
|
+
/** Get current account info. */
|
|
372
|
+
async me() {
|
|
373
|
+
const raw = await this.request("GET", "/v1/me");
|
|
374
|
+
const rawUser = raw.user;
|
|
375
|
+
const rawOrg = raw.organization;
|
|
376
|
+
return {
|
|
377
|
+
user: rawUser ? mapKeys(rawUser, accountUserFieldsFromApi) : null,
|
|
378
|
+
organization: rawOrg ? mapKeys(rawOrg, accountOrgFieldsFromApi) : null
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Verify a webhook signature. */
|
|
382
|
+
verifyWebhook(rawBody, signature, secret) {
|
|
383
|
+
return verifyWebhook(rawBody, signature, secret);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
387
|
+
0 && (module.exports = {
|
|
388
|
+
Fdbck,
|
|
389
|
+
FdbckApiError,
|
|
390
|
+
FdbckError,
|
|
391
|
+
FdbckNetworkError,
|
|
392
|
+
verifyWebhook
|
|
393
|
+
});
|