@plenary/client 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.
@@ -0,0 +1,352 @@
1
+ /** Configuration for {@link createClient}. */
2
+ interface ClientConfig {
3
+ /** CMS origin, e.g. "https://cms.example.com". No trailing `/api/v1`. */
4
+ baseUrl: string;
5
+ /** Site to read from; sent as `?site_key=`. */
6
+ siteKey: string;
7
+ /** Public delivery API key; sent as the `X-API-KEY` header. */
8
+ apiKey: string;
9
+ /** Default locale, always sent as `?locale=` (per CONTENT_API.md). */
10
+ locale: string;
11
+ /** Optional fetch implementation (defaults to global `fetch`). */
12
+ fetch?: typeof fetch;
13
+ }
14
+ /** A content entry. `data` is generic so consumers can type their fields. */
15
+ interface Entry<TData = Record<string, unknown>> {
16
+ id: string;
17
+ site_key: string;
18
+ model_key: string;
19
+ slug: string | null;
20
+ locale: string | null;
21
+ title: string | null;
22
+ status: string;
23
+ published_at: string | null;
24
+ publish_at: string | null;
25
+ unpublish_at: string | null;
26
+ created_at: string;
27
+ updated_at: string;
28
+ /**
29
+ * Content folder placement. `folder_id` is the containing folder (null at the
30
+ * model root); `path` is that folder's directory path ("/" at the root), and
31
+ * excludes the entry's own slug. Shared across an entry's translations. Route
32
+ * on both `path` and `slug` — see the Directory Path section of CONTENT_API.md.
33
+ */
34
+ folder_id: string | null;
35
+ path: string;
36
+ /** Field values keyed by field key; a `blocks` field holds a component tree. */
37
+ data: TData;
38
+ }
39
+ /** Comparison operators supported by `where:` and filter trees. */
40
+ interface WhereCondition {
41
+ $eq?: unknown;
42
+ $ne?: unknown;
43
+ $lt?: unknown;
44
+ $lte?: unknown;
45
+ $gt?: unknown;
46
+ $gte?: unknown;
47
+ /** Value is in the set. */
48
+ $in?: unknown[];
49
+ /** Value is not in the set. */
50
+ $nin?: unknown[];
51
+ /** Case-sensitive substring/pattern match. */
52
+ $like?: string;
53
+ /** Case-insensitive substring/pattern match. */
54
+ $ilike?: string;
55
+ /** Whether the value is null. */
56
+ $is_null?: boolean;
57
+ }
58
+ /**
59
+ * A flat `where:` map: field path → a bare value (shorthand for `$eq`) or a
60
+ * condition object. Field paths for user fields are prefixed `data.`; intrinsic
61
+ * properties (`slug`, `status`, …) are not. AND-combined by the server.
62
+ */
63
+ type Where = Record<string, unknown | WhereCondition>;
64
+ /** A filter tree for the `filters` argument — supports `$and`/`$or` to any depth. */
65
+ interface FilterTree {
66
+ where: Record<string, unknown>;
67
+ }
68
+ type SortDirection = "asc" | "desc";
69
+ /** Sort spec: field path → direction. Applied left-to-right (insertion order). */
70
+ type Sort = Record<string, SortDirection>;
71
+ /**
72
+ * Hydration spec: reference field path → `"*"` (all fields) or an explicit list
73
+ * of fields to expand.
74
+ */
75
+ type Hydrate = Record<string, "*" | string[]>;
76
+ /** Options shared by every read. */
77
+ interface ReadOptions {
78
+ /** Override the client's default locale for this call. */
79
+ locale?: string;
80
+ /** Override the client's site for this call. */
81
+ siteKey?: string;
82
+ /** Expand reference fields. */
83
+ hydrate?: Hydrate;
84
+ /** Return only these fields. Mutually exclusive with `exclude`. */
85
+ include?: string[];
86
+ /** Omit these fields. Mutually exclusive with `include`. */
87
+ exclude?: string[];
88
+ }
89
+ /** Options for {@link PlenaryClient.listEntries}. */
90
+ interface ListOptions extends ReadOptions {
91
+ /** Flat AND filter. Mutually exclusive with `filters`. */
92
+ where?: Where;
93
+ /** Filter tree (for `$or`/`$and`). Mutually exclusive with `where`. */
94
+ filters?: FilterTree;
95
+ sort?: Sort;
96
+ /** Max results (server default 100, max 500). */
97
+ limit?: number;
98
+ offset?: number;
99
+ /** Ask for the total match count, surfaced as `total`. */
100
+ count?: boolean;
101
+ }
102
+ /** Result of a list read. `total` is present only when `count: true` was set. */
103
+ interface ListResult<TData = Record<string, unknown>> {
104
+ items: Entry<TData>[];
105
+ total?: number;
106
+ }
107
+ /** Field types a form model may contain (server allow-list; see docs/FORMS.md). */
108
+ type PublicFormFieldType = "text" | "number" | "boolean" | "date" | "time" | "datetime" | "email" | "url" | "phone" | "select" | "multi_select" | "tags" | "object" | "list" | "json";
109
+ /**
110
+ * One field of a render-safe form definition. `settings`, `ui`, and `validation`
111
+ * are omitted by the server when empty rather than shipped as `{}`.
112
+ */
113
+ interface PublicFormField {
114
+ key: string;
115
+ name: string;
116
+ type: PublicFormFieldType;
117
+ required: boolean;
118
+ settings?: Record<string, unknown>;
119
+ ui?: Record<string, unknown>;
120
+ validation?: Record<string, unknown>;
121
+ }
122
+ /**
123
+ * The render-safe form definition from `GET /api/v1/forms/{form_key}` — the one
124
+ * model-shaped thing a public delivery key may read. Never includes ids,
125
+ * timestamps, site assignments, private fields, or hidden fields.
126
+ */
127
+ interface PublicFormDefinition {
128
+ key: string;
129
+ name: string;
130
+ description?: string;
131
+ fields: PublicFormField[];
132
+ /** Anti-spam time-trap token; when present, echo it back on submit as `ts`. */
133
+ ts?: string;
134
+ }
135
+ /** Options for {@link PlenaryClient.submitForm}. */
136
+ interface FormSubmitOptions {
137
+ /** Override the client's site for this call. */
138
+ siteKey?: string;
139
+ /**
140
+ * Honeypot value — the reserved `website` field a rendered form keeps hidden.
141
+ * Send it verbatim (normally the empty string); a non-empty value marks the
142
+ * entry as spam server-side.
143
+ */
144
+ website?: string;
145
+ /** Time-trap token from {@link PublicFormDefinition.ts}, echoed back verbatim. */
146
+ ts?: string;
147
+ }
148
+ /** Acknowledgement returned by {@link PlenaryClient.submitForm}. */
149
+ interface FormSubmitResult {
150
+ ok: boolean;
151
+ id?: string;
152
+ }
153
+ /** One generated image variant. `url` is empty until the variant is ready. */
154
+ interface MediaVariant {
155
+ status: string;
156
+ url?: string;
157
+ }
158
+ /**
159
+ * A media item's detail, from `GET /api/v1/media/{media_id}`. URLs are
160
+ * installation-relative paths (e.g. `/media/{id}/original.png`).
161
+ */
162
+ interface MediaDetail {
163
+ media_id: string;
164
+ media_type: string;
165
+ mime_type: string;
166
+ file_name: string;
167
+ label: string;
168
+ description: string;
169
+ original_url: string;
170
+ thumb_status?: string;
171
+ thumb_url?: string;
172
+ width?: number;
173
+ height?: number;
174
+ aspect_ratio?: number;
175
+ variants?: Record<string, MediaVariant>;
176
+ }
177
+
178
+ /** The content client returned by {@link createClient}. */
179
+ interface PlenaryClient {
180
+ /** Fetch a singleton model's entry. */
181
+ getSingleton<TData = Record<string, unknown>>(modelKey: string, opts?: ReadOptions): Promise<Entry<TData>>;
182
+ /** Fetch one entry of a collection/page by slug or UUID. */
183
+ getEntry<TData = Record<string, unknown>>(modelKey: string, slugOrId: string, opts?: ReadOptions): Promise<Entry<TData>>;
184
+ /** List entries of a collection/page model. */
185
+ listEntries<TData = Record<string, unknown>>(modelKey: string, opts?: ListOptions): Promise<ListResult<TData>>;
186
+ /**
187
+ * Fetch a form's render-safe definition — the field list a renderer needs,
188
+ * readable with a public delivery key.
189
+ */
190
+ getForm(formKey: string): Promise<PublicFormDefinition>;
191
+ /** Submit a public form. */
192
+ submitForm(formKey: string, data: Record<string, unknown>, opts?: FormSubmitOptions): Promise<FormSubmitResult>;
193
+ /**
194
+ * Fetch a media item's detail (URLs + variants). Requires a key that may read
195
+ * media; a public delivery key cannot, so this is used in authoring/preview
196
+ * contexts or by sites configured with a media-capable key.
197
+ */
198
+ getMedia(mediaId: string): Promise<MediaDetail>;
199
+ }
200
+ declare function createClient(config: ClientConfig): PlenaryClient;
201
+
202
+ /** Base context every request carries: which site and locale. */
203
+ interface QueryBase {
204
+ siteKey: string;
205
+ locale: string;
206
+ }
207
+ /** Builds the query for a single-entry read (get / singleton). */
208
+ declare function buildReadParams(base: QueryBase, opts?: ReadOptions): URLSearchParams;
209
+ /** Builds the query for a list read, including filters, sort, and pagination. */
210
+ declare function buildListParams(base: QueryBase, opts?: ListOptions): URLSearchParams;
211
+
212
+ /** Base error for a non-2xx response from the CMS. */
213
+ declare class PlenaryError extends Error {
214
+ readonly status: number;
215
+ /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */
216
+ readonly code: string;
217
+ /** Interpolation variables the server attached to the code. */
218
+ readonly vars?: Record<string, unknown>;
219
+ constructor(status: number, code: string, vars?: Record<string, unknown>);
220
+ }
221
+ /** A 404 — model or entry not found (or hidden by a private model). */
222
+ declare class PlenaryNotFoundError extends PlenaryError {
223
+ constructor(code: string, vars?: Record<string, unknown>);
224
+ }
225
+ /** A 400/422 — malformed query or invalid input. */
226
+ declare class PlenaryValidationError extends PlenaryError {
227
+ constructor(status: number, code: string, vars?: Record<string, unknown>);
228
+ }
229
+ /** A 403 — the key may not read the requested site/resource. */
230
+ declare class PlenaryForbiddenError extends PlenaryError {
231
+ constructor(code: string, vars?: Record<string, unknown>);
232
+ }
233
+ /** Builds the right error subclass from a failed response's status and body. */
234
+ declare function errorFromResponse(status: number, body: unknown): PlenaryError;
235
+
236
+ /**
237
+ * Webhook consumer helpers: the payload types Plenary POSTs (docs/WEBHOOKS.md)
238
+ * and framework-neutral verification/parsing. Plenary does not sign requests
239
+ * yet; verification is a shared-secret comparison against a header or query
240
+ * parameter. When HMAC signing ships, it slots in here without changing the
241
+ * call sites.
242
+ */
243
+ /** The five entry lifecycle events a content-model webhook can subscribe to. */
244
+ type EntryWebhookEvent = "entry.created" | "entry.updated" | "entry.published" | "entry.unpublished" | "entry.deleted";
245
+ declare const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[];
246
+ /** The event carried by the console's Test button. The body is otherwise an
247
+ * ordinary entry or form payload, built from the model's most recent entry. */
248
+ declare const WEBHOOK_TEST_EVENT = "webhook.test";
249
+ /** Who caused the event. `label` survives the actor's deletion. */
250
+ interface WebhookActor {
251
+ type: "user" | "api_key" | "system";
252
+ id?: string;
253
+ label: string;
254
+ }
255
+ /** The entry's identity. Field selection never touches these. */
256
+ interface WebhookEntryRef {
257
+ id: string;
258
+ site_key?: string;
259
+ locale?: string;
260
+ slug?: string;
261
+ /** Directory path of the entry's folder ("/" at the model root). Does NOT
262
+ * include the entry's own slug — join them to build a URL. */
263
+ path?: string;
264
+ status?: string;
265
+ created_at?: string;
266
+ updated_at?: string;
267
+ published_at?: string;
268
+ }
269
+ /** A form entry's identity: no slug, locale, path, or publish state. */
270
+ interface WebhookFormEntryRef {
271
+ id: string;
272
+ site_key?: string;
273
+ status?: string;
274
+ created_at?: string;
275
+ source?: {
276
+ ip?: string;
277
+ user_agent?: string;
278
+ referer?: string;
279
+ };
280
+ }
281
+ /** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */
282
+ interface EntryWebhookPayload<TData = Record<string, unknown>> {
283
+ event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;
284
+ delivery_id: string;
285
+ occurred_at: string;
286
+ webhook: {
287
+ id: string;
288
+ name?: string;
289
+ };
290
+ model: {
291
+ id: string;
292
+ key?: string;
293
+ };
294
+ entry: WebhookEntryRef;
295
+ actor: WebhookActor;
296
+ /** Paths that differ from the previous revision. Always present; empty for
297
+ * `entry.created` and `entry.deleted`. */
298
+ changed: string[];
299
+ revision_id?: string;
300
+ /** Field values per the webhook's field selection; absent for "no field data". */
301
+ data?: TData;
302
+ }
303
+ /** Payload for `form.submitted` (and form-shaped `webhook.test`). */
304
+ interface FormWebhookPayload<TData = Record<string, unknown>> {
305
+ event: "form.submitted" | typeof WEBHOOK_TEST_EVENT;
306
+ delivery_id: string;
307
+ occurred_at: string;
308
+ webhook: {
309
+ id: string;
310
+ name?: string;
311
+ };
312
+ form: {
313
+ id: string;
314
+ key?: string;
315
+ };
316
+ form_entry: WebhookFormEntryRef;
317
+ data?: TData;
318
+ }
319
+ type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;
320
+ declare function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload;
321
+ declare function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload;
322
+ /** A request that is not a valid webhook delivery. `status` is the HTTP status
323
+ * an endpoint should answer with — 4xx makes the delivery fail fast in the
324
+ * console's delivery log instead of being retried for seven hours. */
325
+ declare class PlenaryWebhookError extends Error {
326
+ readonly status: number;
327
+ constructor(status: number, message: string);
328
+ }
329
+ /** Constant-time comparison of a provided secret against the expected one. */
330
+ declare function verifyWebhookSecret(provided: string | null | undefined, secret: string): boolean;
331
+ /** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}
332
+ * (400) for anything that is not a Plenary webhook payload. */
333
+ declare function parseWebhookPayload(body: unknown): WebhookPayload;
334
+ /** Options for {@link parseWebhookRequest}. */
335
+ interface ParseWebhookOptions {
336
+ /** The shared secret configured on the webhook (as a secret header or query
337
+ * parameter in the console). */
338
+ secret: string;
339
+ /** Header the secret arrives in. Default `x-plenary-secret`. */
340
+ secretHeader?: string;
341
+ /** Query parameter checked when the header is absent. Default `secret`. */
342
+ secretQuery?: string;
343
+ }
344
+ /**
345
+ * Verifies and parses a webhook delivery from a Fetch-API `Request`:
346
+ * POST only, secret in the configured header or query parameter, JSON body of
347
+ * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the
348
+ * status the endpoint should answer (405, 401, or 400).
349
+ */
350
+ declare function parseWebhookRequest(request: Request, options: ParseWebhookOptions): Promise<WebhookPayload>;
351
+
352
+ export { type ClientConfig, ENTRY_WEBHOOK_EVENTS, type Entry, type EntryWebhookEvent, type EntryWebhookPayload, type FilterTree, type FormSubmitOptions, type FormSubmitResult, type FormWebhookPayload, type Hydrate, type ListOptions, type ListResult, type MediaDetail, type MediaVariant, type ParseWebhookOptions, type PlenaryClient, PlenaryError, PlenaryForbiddenError, PlenaryNotFoundError, PlenaryValidationError, PlenaryWebhookError, type PublicFormDefinition, type PublicFormField, type PublicFormFieldType, type QueryBase, type ReadOptions, type Sort, type SortDirection, WEBHOOK_TEST_EVENT, type WebhookActor, type WebhookEntryRef, type WebhookFormEntryRef, type WebhookPayload, type Where, type WhereCondition, buildListParams, buildReadParams, createClient, errorFromResponse, isEntryWebhookPayload, isFormWebhookPayload, parseWebhookPayload, parseWebhookRequest, verifyWebhookSecret };
package/dist/index.js ADDED
@@ -0,0 +1,292 @@
1
+ // src/query.ts
2
+ var OPERATORS = /* @__PURE__ */ new Set([
3
+ "$eq",
4
+ "$ne",
5
+ "$lt",
6
+ "$lte",
7
+ "$gt",
8
+ "$gte",
9
+ "$in",
10
+ "$nin",
11
+ "$like",
12
+ "$ilike",
13
+ "$is_null"
14
+ ]);
15
+ function isCondition(value) {
16
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
17
+ const keys = Object.keys(value);
18
+ return keys.length > 0 && keys.every((k) => OPERATORS.has(k));
19
+ }
20
+ function encodeOperatorValue(op, value) {
21
+ if (op === "$in" || op === "$nin") {
22
+ return Array.isArray(value) ? value.map(String).join(",") : String(value);
23
+ }
24
+ if (op === "$is_null") {
25
+ return String(Boolean(value));
26
+ }
27
+ return String(value);
28
+ }
29
+ function appendWhere(params, where) {
30
+ for (const [field, value] of Object.entries(where)) {
31
+ if (isCondition(value)) {
32
+ for (const [op, opValue] of Object.entries(value)) {
33
+ params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));
34
+ }
35
+ } else {
36
+ params.append(`where:${field}`, String(value));
37
+ }
38
+ }
39
+ }
40
+ function appendSort(params, sort) {
41
+ for (const [field, dir] of Object.entries(sort)) {
42
+ params.append(`sort:${field}`, dir);
43
+ }
44
+ }
45
+ function appendHydrate(params, hydrate) {
46
+ for (const [field, spec] of Object.entries(hydrate)) {
47
+ params.append(`hydrate:${field}`, spec === "*" ? "*" : spec.join(","));
48
+ }
49
+ }
50
+ function appendCommon(params, base, opts) {
51
+ params.set("site_key", opts.siteKey ?? base.siteKey);
52
+ params.set("locale", opts.locale ?? base.locale);
53
+ if (opts.include && opts.exclude) {
54
+ throw new Error("plenary: `include` and `exclude` are mutually exclusive");
55
+ }
56
+ if (opts.include?.length) params.set("include", opts.include.join(","));
57
+ if (opts.exclude?.length) params.set("exclude", opts.exclude.join(","));
58
+ if (opts.hydrate) appendHydrate(params, opts.hydrate);
59
+ }
60
+ function buildReadParams(base, opts = {}) {
61
+ const params = new URLSearchParams();
62
+ appendCommon(params, base, opts);
63
+ return params;
64
+ }
65
+ function buildListParams(base, opts = {}) {
66
+ const params = new URLSearchParams();
67
+ appendCommon(params, base, opts);
68
+ if (opts.where && opts.filters) {
69
+ throw new Error("plenary: `where` and `filters` are mutually exclusive");
70
+ }
71
+ if (opts.filters) {
72
+ params.set("filters", JSON.stringify(opts.filters));
73
+ } else if (opts.where) {
74
+ appendWhere(params, opts.where);
75
+ }
76
+ if (opts.sort) appendSort(params, opts.sort);
77
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
78
+ if (opts.offset !== void 0) params.set("offset", String(opts.offset));
79
+ if (opts.count) params.set("count", "true");
80
+ return params;
81
+ }
82
+
83
+ // src/errors.ts
84
+ var PlenaryError = class extends Error {
85
+ status;
86
+ /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */
87
+ code;
88
+ /** Interpolation variables the server attached to the code. */
89
+ vars;
90
+ constructor(status, code, vars) {
91
+ super(`Plenary request failed (${status}): ${code}`);
92
+ this.name = "PlenaryError";
93
+ this.status = status;
94
+ this.code = code;
95
+ this.vars = vars;
96
+ }
97
+ };
98
+ var PlenaryNotFoundError = class extends PlenaryError {
99
+ constructor(code, vars) {
100
+ super(404, code, vars);
101
+ this.name = "PlenaryNotFoundError";
102
+ }
103
+ };
104
+ var PlenaryValidationError = class extends PlenaryError {
105
+ constructor(status, code, vars) {
106
+ super(status, code, vars);
107
+ this.name = "PlenaryValidationError";
108
+ }
109
+ };
110
+ var PlenaryForbiddenError = class extends PlenaryError {
111
+ constructor(code, vars) {
112
+ super(403, code, vars);
113
+ this.name = "PlenaryForbiddenError";
114
+ }
115
+ };
116
+ function errorFromResponse(status, body) {
117
+ const parsed = body;
118
+ const code = parsed?.error?.code ?? `http_${status}`;
119
+ const vars = parsed?.error?.vars;
120
+ switch (status) {
121
+ case 404:
122
+ return new PlenaryNotFoundError(code, vars);
123
+ case 403:
124
+ return new PlenaryForbiddenError(code, vars);
125
+ case 400:
126
+ case 422:
127
+ return new PlenaryValidationError(status, code, vars);
128
+ default:
129
+ return new PlenaryError(status, code, vars);
130
+ }
131
+ }
132
+
133
+ // src/client.ts
134
+ function joinUrl(baseUrl, path) {
135
+ return `${baseUrl.replace(/\/+$/, "")}${path}`;
136
+ }
137
+ function createClient(config) {
138
+ const base = { siteKey: config.siteKey, locale: config.locale };
139
+ const doFetch = config.fetch ?? globalThis.fetch;
140
+ async function request(method, path, params, body) {
141
+ const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;
142
+ const headers = { "X-API-KEY": config.apiKey };
143
+ if (body !== void 0) headers["Content-Type"] = "application/json";
144
+ const response = await doFetch(url, {
145
+ method,
146
+ headers,
147
+ body: body !== void 0 ? JSON.stringify(body) : void 0
148
+ });
149
+ const parsed = response.status === 204 ? null : await response.json().catch(() => null);
150
+ if (!response.ok) {
151
+ throw errorFromResponse(response.status, parsed);
152
+ }
153
+ return { data: parsed, response };
154
+ }
155
+ return {
156
+ async getSingleton(modelKey, opts = {}) {
157
+ const params = buildReadParams(base, opts);
158
+ const { data } = await request(
159
+ "GET",
160
+ `/api/v1/content/${encodeURIComponent(modelKey)}`,
161
+ params
162
+ );
163
+ return data;
164
+ },
165
+ async getEntry(modelKey, slugOrId, opts = {}) {
166
+ const params = buildReadParams(base, opts);
167
+ const { data } = await request(
168
+ "GET",
169
+ `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,
170
+ params
171
+ );
172
+ return data;
173
+ },
174
+ async listEntries(modelKey, opts = {}) {
175
+ const params = buildListParams(base, opts);
176
+ const { data, response } = await request(
177
+ "GET",
178
+ `/api/v1/content/${encodeURIComponent(modelKey)}`,
179
+ params
180
+ );
181
+ const result = { items: data };
182
+ const total = response.headers.get("X-Total-Count");
183
+ if (total !== null) result.total = Number(total);
184
+ return result;
185
+ },
186
+ async getForm(formKey) {
187
+ const { data } = await request(
188
+ "GET",
189
+ `/api/v1/forms/${encodeURIComponent(formKey)}`,
190
+ new URLSearchParams()
191
+ );
192
+ return data;
193
+ },
194
+ async submitForm(formKey, data, opts = {}) {
195
+ const params = new URLSearchParams();
196
+ params.set("site_key", opts.siteKey ?? base.siteKey);
197
+ const body = { data };
198
+ if (opts.website !== void 0) body.website = opts.website;
199
+ if (opts.ts !== void 0) body.ts = opts.ts;
200
+ const { data: result } = await request(
201
+ "POST",
202
+ `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,
203
+ params,
204
+ body
205
+ );
206
+ return result;
207
+ },
208
+ async getMedia(mediaId) {
209
+ const { data } = await request(
210
+ "GET",
211
+ `/api/v1/media/${encodeURIComponent(mediaId)}`,
212
+ new URLSearchParams()
213
+ );
214
+ return data;
215
+ }
216
+ };
217
+ }
218
+
219
+ // src/webhook.ts
220
+ var ENTRY_WEBHOOK_EVENTS = [
221
+ "entry.created",
222
+ "entry.updated",
223
+ "entry.published",
224
+ "entry.unpublished",
225
+ "entry.deleted"
226
+ ];
227
+ var WEBHOOK_TEST_EVENT = "webhook.test";
228
+ function isEntryWebhookPayload(p) {
229
+ return "entry" in p;
230
+ }
231
+ function isFormWebhookPayload(p) {
232
+ return "form_entry" in p;
233
+ }
234
+ var PlenaryWebhookError = class extends Error {
235
+ status;
236
+ constructor(status, message) {
237
+ super(message);
238
+ this.name = "PlenaryWebhookError";
239
+ this.status = status;
240
+ }
241
+ };
242
+ function verifyWebhookSecret(provided, secret) {
243
+ if (typeof provided !== "string") return false;
244
+ const enc = new TextEncoder();
245
+ const a = enc.encode(provided);
246
+ const b = enc.encode(secret);
247
+ let diff = a.length ^ b.length;
248
+ const len = Math.max(a.length, b.length);
249
+ for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
250
+ return diff === 0;
251
+ }
252
+ function parseWebhookPayload(body) {
253
+ const p = body;
254
+ if (p === null || typeof p !== "object" || typeof p["event"] !== "string" || typeof p["delivery_id"] !== "string" || !("entry" in p || "form_entry" in p)) {
255
+ throw new PlenaryWebhookError(400, "Not a Plenary webhook payload");
256
+ }
257
+ return p;
258
+ }
259
+ async function parseWebhookRequest(request, options) {
260
+ if (request.method !== "POST") {
261
+ throw new PlenaryWebhookError(405, "Method not allowed");
262
+ }
263
+ const header = options.secretHeader ?? "x-plenary-secret";
264
+ const query = options.secretQuery ?? "secret";
265
+ const provided = request.headers.get(header) ?? new URL(request.url).searchParams.get(query);
266
+ if (!verifyWebhookSecret(provided, options.secret)) {
267
+ throw new PlenaryWebhookError(401, "Invalid webhook secret");
268
+ }
269
+ const body = await request.json().catch(() => {
270
+ throw new PlenaryWebhookError(400, "Body is not valid JSON");
271
+ });
272
+ return parseWebhookPayload(body);
273
+ }
274
+ export {
275
+ ENTRY_WEBHOOK_EVENTS,
276
+ PlenaryError,
277
+ PlenaryForbiddenError,
278
+ PlenaryNotFoundError,
279
+ PlenaryValidationError,
280
+ PlenaryWebhookError,
281
+ WEBHOOK_TEST_EVENT,
282
+ buildListParams,
283
+ buildReadParams,
284
+ createClient,
285
+ errorFromResponse,
286
+ isEntryWebhookPayload,
287
+ isFormWebhookPayload,
288
+ parseWebhookPayload,
289
+ parseWebhookRequest,
290
+ verifyWebhookSecret
291
+ };
292
+ //# sourceMappingURL=index.js.map