@plenary/client 0.5.1 → 1.0.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.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -281,7 +281,7 @@ var PlenaryWebhookError = class extends Error {
|
|
|
281
281
|
}
|
|
282
282
|
};
|
|
283
283
|
function verifyWebhookSecret(provided, secret) {
|
|
284
|
-
if (typeof provided !== "string") return false;
|
|
284
|
+
if (typeof provided !== "string" || !secret) return false;
|
|
285
285
|
const enc = new TextEncoder();
|
|
286
286
|
const a = enc.encode(provided);
|
|
287
287
|
const b = enc.encode(secret);
|
|
@@ -301,6 +301,9 @@ async function parseWebhookRequest(request, options) {
|
|
|
301
301
|
if (request.method !== "POST") {
|
|
302
302
|
throw new PlenaryWebhookError(405, "Method not allowed");
|
|
303
303
|
}
|
|
304
|
+
if (!options.secret) {
|
|
305
|
+
throw new PlenaryWebhookError(500, "Webhook secret is not configured");
|
|
306
|
+
}
|
|
304
307
|
const header = options.secretHeader ?? "x-plenary-secret";
|
|
305
308
|
const query = options.secretQuery ?? "secret";
|
|
306
309
|
const provided = request.headers.get(header) ?? new URL(request.url).searchParams.get(query);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["/**\n * @plenary/client — the framework-free Plenary content client: published-read\n * content fetch, public form submission, and a typed query builder. Configured\n * with a public delivery API key; no admin endpoints.\n */\n\nexport { createClient, type PlenaryClient } from \"./client.js\";\nexport {\n buildReadParams,\n buildListParams,\n type QueryBase,\n} from \"./query.js\";\nexport {\n PlenaryError,\n PlenaryNotFoundError,\n PlenaryValidationError,\n PlenaryForbiddenError,\n errorFromResponse,\n} from \"./errors.js\";\nexport {\n ENTRY_WEBHOOK_EVENTS,\n WEBHOOK_TEST_EVENT,\n PlenaryWebhookError,\n verifyWebhookSecret,\n parseWebhookPayload,\n parseWebhookRequest,\n isEntryWebhookPayload,\n isFormWebhookPayload,\n type EntryWebhookEvent,\n type WebhookActor,\n type WebhookEntryRef,\n type WebhookFormEntryRef,\n type EntryWebhookPayload,\n type FormWebhookPayload,\n type WebhookPayload,\n type ParseWebhookOptions,\n} from \"./webhook.js\";\nexport type {\n ClientConfig,\n Entry,\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n SortDirection,\n Hydrate,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormFieldType,\n PublicFormField,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaVariant,\n MediaDetail,\n} from \"./types.js\";\n","import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one. */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\") return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["/**\n * @plenary/client — the framework-free Plenary content client: published-read\n * content fetch, public form submission, and a typed query builder. Configured\n * with a public delivery API key; no admin endpoints.\n */\n\nexport { createClient, type PlenaryClient } from \"./client.js\";\nexport {\n buildReadParams,\n buildListParams,\n type QueryBase,\n} from \"./query.js\";\nexport {\n PlenaryError,\n PlenaryNotFoundError,\n PlenaryValidationError,\n PlenaryForbiddenError,\n errorFromResponse,\n} from \"./errors.js\";\nexport {\n ENTRY_WEBHOOK_EVENTS,\n WEBHOOK_TEST_EVENT,\n PlenaryWebhookError,\n verifyWebhookSecret,\n parseWebhookPayload,\n parseWebhookRequest,\n isEntryWebhookPayload,\n isFormWebhookPayload,\n type EntryWebhookEvent,\n type WebhookActor,\n type WebhookEntryRef,\n type WebhookFormEntryRef,\n type EntryWebhookPayload,\n type FormWebhookPayload,\n type WebhookPayload,\n type ParseWebhookOptions,\n} from \"./webhook.js\";\nexport type {\n ClientConfig,\n Entry,\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n SortDirection,\n Hydrate,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormFieldType,\n PublicFormField,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaVariant,\n MediaDetail,\n} from \"./types.js\";\n","import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -326,7 +326,9 @@ declare class PlenaryWebhookError extends Error {
|
|
|
326
326
|
readonly status: number;
|
|
327
327
|
constructor(status: number, message: string);
|
|
328
328
|
}
|
|
329
|
-
/** Constant-time comparison of a provided secret against the expected one.
|
|
329
|
+
/** Constant-time comparison of a provided secret against the expected one.
|
|
330
|
+
* An empty or missing `secret` never matches, so an unset environment
|
|
331
|
+
* variable can't turn into "accept an empty header". */
|
|
330
332
|
declare function verifyWebhookSecret(provided: string | null | undefined, secret: string): boolean;
|
|
331
333
|
/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}
|
|
332
334
|
* (400) for anything that is not a Plenary webhook payload. */
|
package/dist/index.d.ts
CHANGED
|
@@ -326,7 +326,9 @@ declare class PlenaryWebhookError extends Error {
|
|
|
326
326
|
readonly status: number;
|
|
327
327
|
constructor(status: number, message: string);
|
|
328
328
|
}
|
|
329
|
-
/** Constant-time comparison of a provided secret against the expected one.
|
|
329
|
+
/** Constant-time comparison of a provided secret against the expected one.
|
|
330
|
+
* An empty or missing `secret` never matches, so an unset environment
|
|
331
|
+
* variable can't turn into "accept an empty header". */
|
|
330
332
|
declare function verifyWebhookSecret(provided: string | null | undefined, secret: string): boolean;
|
|
331
333
|
/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}
|
|
332
334
|
* (400) for anything that is not a Plenary webhook payload. */
|
package/dist/index.js
CHANGED
|
@@ -240,7 +240,7 @@ var PlenaryWebhookError = class extends Error {
|
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
242
|
function verifyWebhookSecret(provided, secret) {
|
|
243
|
-
if (typeof provided !== "string") return false;
|
|
243
|
+
if (typeof provided !== "string" || !secret) return false;
|
|
244
244
|
const enc = new TextEncoder();
|
|
245
245
|
const a = enc.encode(provided);
|
|
246
246
|
const b = enc.encode(secret);
|
|
@@ -260,6 +260,9 @@ async function parseWebhookRequest(request, options) {
|
|
|
260
260
|
if (request.method !== "POST") {
|
|
261
261
|
throw new PlenaryWebhookError(405, "Method not allowed");
|
|
262
262
|
}
|
|
263
|
+
if (!options.secret) {
|
|
264
|
+
throw new PlenaryWebhookError(500, "Webhook secret is not configured");
|
|
265
|
+
}
|
|
263
266
|
const header = options.secretHeader ?? "x-plenary-secret";
|
|
264
267
|
const query = options.secretQuery ?? "secret";
|
|
265
268
|
const provided = request.headers.get(header) ?? new URL(request.url).searchParams.get(query);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one. */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\") return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/query.ts","../src/errors.ts","../src/client.ts","../src/webhook.ts"],"sourcesContent":["import type {\n Where,\n WhereCondition,\n FilterTree,\n Sort,\n Hydrate,\n ReadOptions,\n ListOptions,\n} from \"./types.js\";\n\n/** The known `where:` operators; anything else on a condition object is passed through. */\nconst OPERATORS = new Set([\n \"$eq\",\n \"$ne\",\n \"$lt\",\n \"$lte\",\n \"$gt\",\n \"$gte\",\n \"$in\",\n \"$nin\",\n \"$like\",\n \"$ilike\",\n \"$is_null\",\n]);\n\n/** A value is a condition when it's a plain object whose keys are all operators. */\nfunction isCondition(value: unknown): value is WhereCondition {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return keys.length > 0 && keys.every((k) => OPERATORS.has(k));\n}\n\nfunction encodeOperatorValue(op: string, value: unknown): string {\n if (op === \"$in\" || op === \"$nin\") {\n return Array.isArray(value) ? value.map(String).join(\",\") : String(value);\n }\n if (op === \"$is_null\") {\n return String(Boolean(value));\n }\n return String(value);\n}\n\nfunction appendWhere(params: URLSearchParams, where: Where): void {\n for (const [field, value] of Object.entries(where)) {\n if (isCondition(value)) {\n for (const [op, opValue] of Object.entries(value)) {\n params.append(`where:${field}.${op}`, encodeOperatorValue(op, opValue));\n }\n } else {\n // Bare value — the server assumes `$eq`.\n params.append(`where:${field}`, String(value));\n }\n }\n}\n\nfunction appendSort(params: URLSearchParams, sort: Sort): void {\n // Insertion order is the sort precedence (left-to-right).\n for (const [field, dir] of Object.entries(sort)) {\n params.append(`sort:${field}`, dir);\n }\n}\n\nfunction appendHydrate(params: URLSearchParams, hydrate: Hydrate): void {\n for (const [field, spec] of Object.entries(hydrate)) {\n params.append(`hydrate:${field}`, spec === \"*\" ? \"*\" : spec.join(\",\"));\n }\n}\n\n/** Base context every request carries: which site and locale. */\nexport interface QueryBase {\n siteKey: string;\n locale: string;\n}\n\nfunction appendCommon(params: URLSearchParams, base: QueryBase, opts: ReadOptions): void {\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n params.set(\"locale\", opts.locale ?? base.locale);\n\n if (opts.include && opts.exclude) {\n throw new Error(\"plenary: `include` and `exclude` are mutually exclusive\");\n }\n if (opts.include?.length) params.set(\"include\", opts.include.join(\",\"));\n if (opts.exclude?.length) params.set(\"exclude\", opts.exclude.join(\",\"));\n if (opts.hydrate) appendHydrate(params, opts.hydrate);\n}\n\n/** Builds the query for a single-entry read (get / singleton). */\nexport function buildReadParams(base: QueryBase, opts: ReadOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n return params;\n}\n\n/** Builds the query for a list read, including filters, sort, and pagination. */\nexport function buildListParams(base: QueryBase, opts: ListOptions = {}): URLSearchParams {\n const params = new URLSearchParams();\n appendCommon(params, base, opts);\n\n if (opts.where && opts.filters) {\n throw new Error(\"plenary: `where` and `filters` are mutually exclusive\");\n }\n if (opts.filters) {\n params.set(\"filters\", JSON.stringify(opts.filters satisfies FilterTree));\n } else if (opts.where) {\n appendWhere(params, opts.where);\n }\n\n if (opts.sort) appendSort(params, opts.sort);\n if (opts.limit !== undefined) params.set(\"limit\", String(opts.limit));\n if (opts.offset !== undefined) params.set(\"offset\", String(opts.offset));\n if (opts.count) params.set(\"count\", \"true\");\n\n return params;\n}\n","/** The server's error envelope: `{ error: { code, vars? } }`. */\ninterface ServerErrorBody {\n error?: { code?: string; vars?: Record<string, unknown> };\n}\n\n/** Base error for a non-2xx response from the CMS. */\nexport class PlenaryError extends Error {\n readonly status: number;\n /** The server's i18n error code (e.g. `entry_query_limit_exceeded`), if any. */\n readonly code: string;\n /** Interpolation variables the server attached to the code. */\n readonly vars?: Record<string, unknown>;\n\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(`Plenary request failed (${status}): ${code}`);\n this.name = \"PlenaryError\";\n this.status = status;\n this.code = code;\n this.vars = vars;\n }\n}\n\n/** A 404 — model or entry not found (or hidden by a private model). */\nexport class PlenaryNotFoundError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(404, code, vars);\n this.name = \"PlenaryNotFoundError\";\n }\n}\n\n/** A 400/422 — malformed query or invalid input. */\nexport class PlenaryValidationError extends PlenaryError {\n constructor(status: number, code: string, vars?: Record<string, unknown>) {\n super(status, code, vars);\n this.name = \"PlenaryValidationError\";\n }\n}\n\n/** A 403 — the key may not read the requested site/resource. */\nexport class PlenaryForbiddenError extends PlenaryError {\n constructor(code: string, vars?: Record<string, unknown>) {\n super(403, code, vars);\n this.name = \"PlenaryForbiddenError\";\n }\n}\n\n/** Builds the right error subclass from a failed response's status and body. */\nexport function errorFromResponse(status: number, body: unknown): PlenaryError {\n const parsed = body as ServerErrorBody | null;\n const code = parsed?.error?.code ?? `http_${status}`;\n const vars = parsed?.error?.vars;\n switch (status) {\n case 404:\n return new PlenaryNotFoundError(code, vars);\n case 403:\n return new PlenaryForbiddenError(code, vars);\n case 400:\n case 422:\n return new PlenaryValidationError(status, code, vars);\n default:\n return new PlenaryError(status, code, vars);\n }\n}\n","import type {\n ClientConfig,\n Entry,\n ReadOptions,\n ListOptions,\n ListResult,\n PublicFormDefinition,\n FormSubmitOptions,\n FormSubmitResult,\n MediaDetail,\n} from \"./types.js\";\nimport { buildReadParams, buildListParams, type QueryBase } from \"./query.js\";\nimport { errorFromResponse } from \"./errors.js\";\n\n/** The content client returned by {@link createClient}. */\nexport interface PlenaryClient {\n /** Fetch a singleton model's entry. */\n getSingleton<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** Fetch one entry of a collection/page by slug or UUID. */\n getEntry<TData = Record<string, unknown>>(\n modelKey: string,\n slugOrId: string,\n opts?: ReadOptions,\n ): Promise<Entry<TData>>;\n\n /** List entries of a collection/page model. */\n listEntries<TData = Record<string, unknown>>(\n modelKey: string,\n opts?: ListOptions,\n ): Promise<ListResult<TData>>;\n\n /**\n * Fetch a form's render-safe definition — the field list a renderer needs,\n * readable with a public delivery key.\n */\n getForm(formKey: string): Promise<PublicFormDefinition>;\n\n /** Submit a public form. */\n submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts?: FormSubmitOptions,\n ): Promise<FormSubmitResult>;\n\n /**\n * Fetch a media item's detail (URLs + variants). Requires a key that may read\n * media; a public delivery key cannot, so this is used in authoring/preview\n * contexts or by sites configured with a media-capable key.\n */\n getMedia(mediaId: string): Promise<MediaDetail>;\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}${path}`;\n}\n\nexport function createClient(config: ClientConfig): PlenaryClient {\n const base: QueryBase = { siteKey: config.siteKey, locale: config.locale };\n const doFetch = config.fetch ?? globalThis.fetch;\n\n async function request<T>(\n method: string,\n path: string,\n params: URLSearchParams,\n body?: unknown,\n ): Promise<{ data: T; response: Response }> {\n const url = `${joinUrl(config.baseUrl, path)}?${params.toString()}`;\n const headers: Record<string, string> = { \"X-API-KEY\": config.apiKey };\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await doFetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const parsed = response.status === 204 ? null : await response.json().catch(() => null);\n if (!response.ok) {\n throw errorFromResponse(response.status, parsed);\n }\n return { data: parsed as T, response };\n }\n\n return {\n async getSingleton<TData>(modelKey: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n return data;\n },\n\n async getEntry<TData>(modelKey: string, slugOrId: string, opts: ReadOptions = {}) {\n const params = buildReadParams(base, opts);\n const { data } = await request<Entry<TData>>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}/${encodeURIComponent(slugOrId)}`,\n params,\n );\n return data;\n },\n\n async listEntries<TData>(modelKey: string, opts: ListOptions = {}) {\n const params = buildListParams(base, opts);\n const { data, response } = await request<Entry<TData>[]>(\n \"GET\",\n `/api/v1/content/${encodeURIComponent(modelKey)}`,\n params,\n );\n const result: ListResult<TData> = { items: data };\n const total = response.headers.get(\"X-Total-Count\");\n if (total !== null) result.total = Number(total);\n return result;\n },\n\n async getForm(formKey: string) {\n const { data } = await request<PublicFormDefinition>(\n \"GET\",\n `/api/v1/forms/${encodeURIComponent(formKey)}`,\n new URLSearchParams(),\n );\n return data;\n },\n\n async submitForm(\n formKey: string,\n data: Record<string, unknown>,\n opts: FormSubmitOptions = {},\n ) {\n const params = new URLSearchParams();\n params.set(\"site_key\", opts.siteKey ?? base.siteKey);\n const body: Record<string, unknown> = { data };\n if (opts.website !== undefined) body.website = opts.website;\n if (opts.ts !== undefined) body.ts = opts.ts;\n const { data: result } = await request<FormSubmitResult>(\n \"POST\",\n `/api/v1/forms/${encodeURIComponent(formKey)}/entries`,\n params,\n body,\n );\n return result;\n },\n\n async getMedia(mediaId: string) {\n const { data } = await request<MediaDetail>(\n \"GET\",\n `/api/v1/media/${encodeURIComponent(mediaId)}`,\n new URLSearchParams(),\n );\n return data;\n },\n };\n}\n","/**\n * Webhook consumer helpers: the payload types Plenary POSTs\n * and framework-neutral verification/parsing. Plenary does not sign requests\n * yet; verification is a shared-secret comparison against a header or query\n * parameter. When HMAC signing ships, it slots in here without changing the\n * call sites.\n */\n\n/** The five entry lifecycle events a content-model webhook can subscribe to. */\nexport type EntryWebhookEvent =\n | \"entry.created\"\n | \"entry.updated\"\n | \"entry.published\"\n | \"entry.unpublished\"\n | \"entry.deleted\";\n\nexport const ENTRY_WEBHOOK_EVENTS: readonly EntryWebhookEvent[] = [\n \"entry.created\",\n \"entry.updated\",\n \"entry.published\",\n \"entry.unpublished\",\n \"entry.deleted\",\n];\n\n/** The event carried by the console's Test button. The body is otherwise an\n * ordinary entry or form payload, built from the model's most recent entry. */\nexport const WEBHOOK_TEST_EVENT = \"webhook.test\";\n\n/** Who caused the event. `label` survives the actor's deletion. */\nexport interface WebhookActor {\n type: \"user\" | \"api_key\" | \"system\";\n id?: string;\n label: string;\n}\n\n/** The entry's identity. Field selection never touches these. */\nexport interface WebhookEntryRef {\n id: string;\n site_key?: string;\n locale?: string;\n slug?: string;\n /** Directory path of the entry's folder (\"/\" at the model root). Does NOT\n * include the entry's own slug — join them to build a URL. */\n path?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n published_at?: string;\n}\n\n/** A form entry's identity: no slug, locale, path, or publish state. */\nexport interface WebhookFormEntryRef {\n id: string;\n site_key?: string;\n status?: string;\n created_at?: string;\n source?: { ip?: string; user_agent?: string; referer?: string };\n}\n\n/** Payload for every `entry.*` event (and entry-shaped `webhook.test`). */\nexport interface EntryWebhookPayload<TData = Record<string, unknown>> {\n event: EntryWebhookEvent | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n model: { id: string; key?: string };\n entry: WebhookEntryRef;\n actor: WebhookActor;\n /** Paths that differ from the previous revision. Always present; empty for\n * `entry.created` and `entry.deleted`. */\n changed: string[];\n revision_id?: string;\n /** Field values per the webhook's field selection; absent for \"no field data\". */\n data?: TData;\n}\n\n/** Payload for `form.submitted` (and form-shaped `webhook.test`). */\nexport interface FormWebhookPayload<TData = Record<string, unknown>> {\n event: \"form.submitted\" | typeof WEBHOOK_TEST_EVENT;\n delivery_id: string;\n occurred_at: string;\n webhook: { id: string; name?: string };\n form: { id: string; key?: string };\n form_entry: WebhookFormEntryRef;\n data?: TData;\n}\n\nexport type WebhookPayload = EntryWebhookPayload | FormWebhookPayload;\n\nexport function isEntryWebhookPayload(p: WebhookPayload): p is EntryWebhookPayload {\n return \"entry\" in p;\n}\n\nexport function isFormWebhookPayload(p: WebhookPayload): p is FormWebhookPayload {\n return \"form_entry\" in p;\n}\n\n/** A request that is not a valid webhook delivery. `status` is the HTTP status\n * an endpoint should answer with — 4xx makes the delivery fail fast in the\n * console's delivery log instead of being retried for seven hours. */\nexport class PlenaryWebhookError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = \"PlenaryWebhookError\";\n this.status = status;\n }\n}\n\n/** Constant-time comparison of a provided secret against the expected one.\n * An empty or missing `secret` never matches, so an unset environment\n * variable can't turn into \"accept an empty header\". */\nexport function verifyWebhookSecret(\n provided: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof provided !== \"string\" || !secret) return false;\n const enc = new TextEncoder();\n const a = enc.encode(provided);\n const b = enc.encode(secret);\n let diff = a.length ^ b.length;\n const len = Math.max(a.length, b.length);\n for (let i = 0; i < len; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);\n return diff === 0;\n}\n\n/** Shape-checks a parsed JSON body and types it. Throws {@link PlenaryWebhookError}\n * (400) for anything that is not a Plenary webhook payload. */\nexport function parseWebhookPayload(body: unknown): WebhookPayload {\n const p = body as Record<string, unknown> | null;\n if (\n p === null ||\n typeof p !== \"object\" ||\n typeof p[\"event\"] !== \"string\" ||\n typeof p[\"delivery_id\"] !== \"string\" ||\n !(\"entry\" in p || \"form_entry\" in p)\n ) {\n throw new PlenaryWebhookError(400, \"Not a Plenary webhook payload\");\n }\n return p as unknown as WebhookPayload;\n}\n\n/** Options for {@link parseWebhookRequest}. */\nexport interface ParseWebhookOptions {\n /** The shared secret configured on the webhook (as a secret header or query\n * parameter in the console). */\n secret: string;\n /** Header the secret arrives in. Default `x-plenary-secret`. */\n secretHeader?: string;\n /** Query parameter checked when the header is absent. Default `secret`. */\n secretQuery?: string;\n}\n\n/**\n * Verifies and parses a webhook delivery from a Fetch-API `Request`:\n * POST only, secret in the configured header or query parameter, JSON body of\n * one of the two payload shapes. Throws {@link PlenaryWebhookError} with the\n * status the endpoint should answer (405, 401, or 400).\n */\nexport async function parseWebhookRequest(\n request: Request,\n options: ParseWebhookOptions,\n): Promise<WebhookPayload> {\n if (request.method !== \"POST\") {\n throw new PlenaryWebhookError(405, \"Method not allowed\");\n }\n if (!options.secret) {\n throw new PlenaryWebhookError(500, \"Webhook secret is not configured\");\n }\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n const provided =\n request.headers.get(header) ?? new URL(request.url).searchParams.get(query);\n if (!verifyWebhookSecret(provided, options.secret)) {\n throw new PlenaryWebhookError(401, \"Invalid webhook secret\");\n }\n const body = await request.json().catch(() => {\n throw new PlenaryWebhookError(400, \"Body is not valid JSON\");\n });\n return parseWebhookPayload(body);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plenary/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Plenary framework-free API client: content fetch, form submission, and query builder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@plenary/core": "0.
|
|
21
|
+
"@plenary/core": "1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"scripts": {
|