chaingrow-blog 0.1.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # chaingrow-blog
2
2
 
3
- Drop-in Next.js SDK for rendering blogs published from [ChainGrow](https://chaingrow.ai).
3
+ Drop-in Next.js SDK for rendering blogs published from [ChainGrow](https://chaingrow.de).
4
4
 
5
5
  - **Zero database.** Your site stores nothing — ChainGrow is the source of truth.
6
6
  - **Instant publishing.** When you press Publish in ChainGrow, the SDK's webhook handler invalidates the relevant page via `revalidateTag`. Next request serves fresh HTML.
@@ -18,18 +18,20 @@ Requires Next.js ≥ 14 (App Router) and React ≥ 18.
18
18
 
19
19
  ## Setup (3 files)
20
20
 
21
- ### 1. Environment variables
21
+ ### 1. Environment variable
22
22
 
23
23
  ```env
24
- CHAINGROW_API_URL=https://api.chaingrow.ai
25
24
  CHAINGROW_API_KEY=ck_live_xxx
26
- CHAINGROW_WEBHOOK_SECRET=whsec_xxx
27
25
  ```
28
26
 
29
- Generate the API key + webhook secret in your ChainGrow dashboard under **Settings → API Keys**. Set the webhook URL on the key to `https://your-site.com/api/chaingrow/webhook`.
27
+ That's the only required variable. Generate the key in your ChainGrow dashboard under **Integrations → API Keys**. It authenticates your API calls AND verifies incoming webhooks, so there's only one secret to manage. The webhook delivery URL is derived automatically from the website you set on your business profile, so you never paste a URL anywhere.
28
+
29
+ (Optional: set `CHAINGROW_API_URL` if you're pointing at staging or a self-hosted deploy. Defaults to `https://api.chaingrow.de`.)
30
30
 
31
31
  ### 2. Webhook receiver
32
32
 
33
+ Mount the SDK's handler at the convention path `/api/chaingrow/webhook` — ChainGrow always POSTs to `${your-website}/api/chaingrow/webhook`:
34
+
33
35
  ```ts
34
36
  // app/api/chaingrow/webhook/route.ts
35
37
  export { POST } from 'chaingrow-blog/next/webhook';
@@ -37,11 +39,11 @@ export { POST } from 'chaingrow-blog/next/webhook';
37
39
 
38
40
  ### 3. Blog page
39
41
 
40
- **Option A — full design control:**
42
+ **Option A — Custom rendering (bring your own JSX):**
41
43
 
42
44
  ```tsx
43
45
  // app/blog/[slug]/page.tsx
44
- import { notFound } from 'next/navigation';
46
+ import { notFound } from 'next/navigation'; // Next.js built-in — serves a real HTTP 404 and renders app/not-found.tsx
45
47
  import { getBlog } from 'chaingrow-blog/next';
46
48
 
47
49
  export default async function BlogPage({
@@ -64,7 +66,7 @@ export default async function BlogPage({
64
66
  }
65
67
  ```
66
68
 
67
- **Option B — default renderer with per-block overrides:**
69
+ **Option B — Default renderer with per-block overrides:**
68
70
 
69
71
  ```tsx
70
72
  import { notFound } from 'next/navigation';
@@ -123,9 +125,13 @@ Renders all `blog.blocks` using unstyled default components. Every block type is
123
125
 
124
126
  Block types: `heading`, `paragraph`, `list`, `quote`, `code`, `image_placeholder`.
125
127
 
126
- ### `createWebhookHandler({ secret?, onEvent? })`
128
+ ### `createWebhookHandler({ apiKey?, onEvent? })`
129
+
130
+ Returns a `{ POST }` route handler. Verifies the `X-ChainGrow-Signature` HMAC (signed server-side with `sha256(apiKey)`) and calls `revalidateTag`. Pass `onEvent` to run custom logic after revalidation (e.g. cache warming, analytics). By default reads `CHAINGROW_API_KEY` from the environment.
131
+
132
+ ### Rate-limit handling
127
133
 
128
- Returns a `{ POST }` route handler. Verifies the `X-ChainGrow-Signature` HMAC and calls `revalidateTag`. Pass `onEvent` to run custom logic after revalidation (e.g. cache warming, analytics).
134
+ Calls are transparently retried once on HTTP `429 Too Many Requests`, honoring the `Retry-After` header. No configuration needed. Other errors bubble up unchanged.
129
135
 
130
136
  ## How publishing works
131
137
 
@@ -156,6 +162,85 @@ Fresh HTML getBlog() → GET /api/v1/blogs/by-slug/<s
156
162
  cached for next visitors
157
163
  ```
158
164
 
165
+ ## Releasing new versions (maintainers only)
166
+
167
+ > Consumers installing `chaingrow-blog` can ignore this section — it's internal notes for ChainGrow maintainers cutting a new release.
168
+
169
+ The SDK is published to npm by a GitHub Actions workflow (`.github/workflows/publish-blog-sdk.yml`). The workflow only runs when you push a **git tag** matching `blog-sdk-v*`. Regular commits and branch pushes do **nothing** — npm is never touched unless you explicitly create and push a release tag.
170
+
171
+ ### Triggers
172
+
173
+ ```yaml
174
+ on:
175
+ push:
176
+ tags:
177
+ - 'blog-sdk-v*' # only fires on matching tags
178
+ workflow_dispatch: # manual "Run workflow" button in GitHub Actions UI
179
+ ```
180
+
181
+ That's it. Two ways to trigger a publish:
182
+
183
+ 1. Push a tag named `blog-sdk-v<version>` (the normal flow)
184
+ 2. Click "Run workflow" on the Actions page in GitHub (manual escape hatch)
185
+
186
+ No other event publishes. You can push commits to `main` all day without releasing.
187
+
188
+ ### Cutting a release
189
+
190
+ ```bash
191
+ # 1. Bump the version in packages/blog-sdk/package.json
192
+ # "version": "0.1.0" → "version": "0.1.1"
193
+
194
+ # 2. Commit the version bump
195
+ git add packages/blog-sdk/package.json
196
+ git commit -m "chore(blog-sdk): release v0.1.1"
197
+ git push # normal push, nothing publishes yet
198
+
199
+ # 3. Create the release tag on that commit
200
+ git tag blog-sdk-v0.1.1
201
+
202
+ # 4. Push the tag (separate command — git push alone doesn't push tags)
203
+ git push origin blog-sdk-v0.1.1 # this is what fires the workflow
204
+ ```
205
+
206
+ About a minute later the new version lands on [npmjs.com/package/chaingrow-blog](https://www.npmjs.com/package/chaingrow-blog).
207
+
208
+ ### Why tags and not branches?
209
+
210
+ Tags are immutable bookmarks pointing at a specific commit, and git treats them as a separate namespace from branches. Splitting the release trigger onto tags means:
211
+
212
+ - Normal development pushes never accidentally publish
213
+ - Every published version has a matching tag you can `git checkout` to see exactly what was shipped
214
+ - Cutting a release is an explicit, deliberate action (`git tag` + `git push origin <tag>`), not a side effect of merging to main
215
+
216
+ ### Versioning rules
217
+
218
+ Follow [semver](https://semver.org/):
219
+
220
+ | Change | Bump |
221
+ |---|---|
222
+ | Adding a new field to `Blog` (backwards-compatible) | patch (`0.1.0` → `0.1.1`) |
223
+ | Adding a new exported function or component | minor (`0.1.0` → `0.2.0`) |
224
+ | **Changing the `Block` type union** | **major (`0.1.0` → `1.0.0`)** — customer override maps will break |
225
+ | Removing or renaming any exported symbol | major |
226
+
227
+ ### Fixing a mistake
228
+
229
+ If you tagged the wrong commit and it hasn't published yet:
230
+
231
+ ```bash
232
+ git tag -d blog-sdk-v0.1.1 # delete local tag
233
+ git push origin :refs/tags/blog-sdk-v0.1.1 # delete remote tag
234
+ # then re-tag correctly
235
+ ```
236
+
237
+ If it already published: npm blocks unpublishing after 72 hours. The recipe is to deprecate the bad version and bump:
238
+
239
+ ```bash
240
+ npm deprecate chaingrow-blog@0.1.1 "published in error — use 0.1.2"
241
+ # then cut a fresh release with the fix
242
+ ```
243
+
159
244
  ## License
160
245
 
161
246
  MIT
@@ -3,7 +3,9 @@ function createClient(opts) {
3
3
  const baseUrl = opts.apiUrl.replace(/\/$/, "");
4
4
  const doFetch = opts.fetch ?? fetch;
5
5
  const request = async (path, init) => {
6
- const res = await doFetch(`${baseUrl}${path}`, {
6
+ const url = `${baseUrl}${path}`;
7
+ const method = init?.method ?? "GET";
8
+ const buildInit = () => ({
7
9
  ...init,
8
10
  headers: {
9
11
  "X-API-Key": opts.apiKey,
@@ -11,10 +13,30 @@ function createClient(opts) {
11
13
  ...init?.headers ?? {}
12
14
  }
13
15
  });
16
+ let res;
17
+ try {
18
+ res = await doFetch(url, buildInit());
19
+ if (res.status === 429) {
20
+ const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
21
+ await new Promise(
22
+ (resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
23
+ );
24
+ res = await doFetch(url, buildInit());
25
+ }
26
+ } catch (err) {
27
+ const cause = err instanceof Error ? err.cause : void 0;
28
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
29
+ const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
30
+ const e = new Error(
31
+ `chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
32
+ );
33
+ e.cause = err;
34
+ throw e;
35
+ }
14
36
  if (res.status === 404) return null;
15
37
  if (!res.ok) {
16
38
  throw new Error(
17
- `chaingrow-blog: ${init?.method ?? "GET"} ${path} failed (${res.status})`
39
+ `chaingrow-blog: ${method} ${url} failed (${res.status})`
18
40
  );
19
41
  }
20
42
  return await res.json();
@@ -49,4 +71,4 @@ function createClient(opts) {
49
71
  export {
50
72
  createClient
51
73
  };
52
- //# sourceMappingURL=chunk-TDHCPX4T.js.map
74
+ //# sourceMappingURL=chunk-OTLGKSYD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response;\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n } catch (err) {\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const cause = err instanceof Error ? err.cause : undefined;\n const code =\n cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":";AAmBO,SAAS,aAAa,MAAsC;AACjE,QAAM,UAAU,KAAK,OAAO,QAAQ,OAAO,EAAE;AAC7C,QAAM,UAAU,KAAK,SAAS;AAE9B,QAAM,UAAU,OACd,MACA,SACsB;AACtB,UAAM,MAAM,GAAG,OAAO,GAAG,IAAI;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,YAAY,OAAoB;AAAA,MACpC,GAAG;AAAA,MACH,SAAS;AAAA,QACP,aAAa,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,GAAI,MAAM,WAAW,CAAC;AAAA,MACxB;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,QAAQ,KAAK,UAAU,CAAC;AAGpC,UAAI,IAAI,WAAW,KAAK;AACtB,cAAM,UAAU,SAAS,IAAI,QAAQ,IAAI,aAAa,KAAK,KAAK,EAAE;AAClE,cAAM,IAAI;AAAA,UAAQ,CAAC,YACjB,WAAW,SAAS,KAAK,IAAI,GAAG,OAAO,IAAI,GAAI;AAAA,QACjD;AACA,cAAM,MAAM,QAAQ,KAAK,UAAU,CAAC;AAAA,MACtC;AAAA,IACF,SAAS,KAAK;AAKZ,YAAM,QAAQ,eAAe,QAAQ,IAAI,QAAQ;AACjD,YAAM,OACJ,SAAS,OAAO,UAAU,YAAY,UAAU,QAC5C,OAAQ,MAA6B,IAAI,IACzC;AACN,YAAM,SAAS,OACX,GAAG,IAAI,GAAG,eAAe,SAAS,IAAI,UAAU,KAAK,IAAI,OAAO,MAAM,EAAE,KACxE,eAAe,QACb,IAAI,UACJ,OAAO,GAAG;AAChB,YAAM,IAAI,IAAI;AAAA,QACZ,mCAAmC,MAAM,IAAI,GAAG,WAAM,MAAM;AAAA,MAE9D;AACA,MAAC,EAAkC,QAAQ;AAC3C,YAAM;AAAA,IACR;AAEA,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,mBAAmB,MAAM,IAAI,GAAG,YAAY,IAAI,MAAM;AAAA,MACxD;AAAA,IACF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,UAAU;AAC5B,YAAM,KAAK,WAAW,aAAa,mBAAmB,QAAQ,CAAC,KAAK;AACpE,aAAO;AAAA,QACL,yBAAyB,mBAAmB,IAAI,CAAC,GAAG,EAAE;AAAA,MACxD;AAAA,IACF;AAAA,IAEA,MAAM,YAAY,IAAI;AACpB,aAAO,QAAc,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,IAChE;AAAA,IAEA,MAAM,UAAU,WAAW,CAAC,GAAG;AAC7B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,SAAS,MAAM;AACzD,YAAM,KAAK,OAAO,SAAS;AAC3B,YAAM,WAAW,MAAM;AAAA,QACrB,gBAAgB,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,MACpC;AACA,UAAI,CAAC,SAAU,QAAO,CAAC;AACvB,YAAM,QAAQ,SAAS,QAAQ,CAAC;AAChC,UAAI,SAAS,UAAU;AACrB,eAAO,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,QAAQ;AAAA,MAC7D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
package/dist/index.cjs CHANGED
@@ -29,7 +29,9 @@ function createClient(opts) {
29
29
  const baseUrl = opts.apiUrl.replace(/\/$/, "");
30
30
  const doFetch = opts.fetch ?? fetch;
31
31
  const request = async (path, init) => {
32
- const res = await doFetch(`${baseUrl}${path}`, {
32
+ const url = `${baseUrl}${path}`;
33
+ const method = init?.method ?? "GET";
34
+ const buildInit = () => ({
33
35
  ...init,
34
36
  headers: {
35
37
  "X-API-Key": opts.apiKey,
@@ -37,10 +39,30 @@ function createClient(opts) {
37
39
  ...init?.headers ?? {}
38
40
  }
39
41
  });
42
+ let res;
43
+ try {
44
+ res = await doFetch(url, buildInit());
45
+ if (res.status === 429) {
46
+ const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
47
+ await new Promise(
48
+ (resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
49
+ );
50
+ res = await doFetch(url, buildInit());
51
+ }
52
+ } catch (err) {
53
+ const cause = err instanceof Error ? err.cause : void 0;
54
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
55
+ const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
56
+ const e = new Error(
57
+ `chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
58
+ );
59
+ e.cause = err;
60
+ throw e;
61
+ }
40
62
  if (res.status === 404) return null;
41
63
  if (!res.ok) {
42
64
  throw new Error(
43
- `chaingrow-blog: ${init?.method ?? "GET"} ${path} failed (${res.status})`
65
+ `chaingrow-blog: ${method} ${url} failed (${res.status})`
44
66
  );
45
67
  }
46
68
  return await res.json();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const res = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${init?.method ?? 'GET'} ${path} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmBO,SAAS,aAAa,MAAsC;AACjE,QAAM,UAAU,KAAK,OAAO,QAAQ,OAAO,EAAE;AAC7C,QAAM,UAAU,KAAK,SAAS;AAE9B,QAAM,UAAU,OACd,MACA,SACsB;AACtB,UAAM,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,MAC7C,GAAG;AAAA,MACH,SAAS;AAAA,QACP,aAAa,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,GAAI,MAAM,WAAW,CAAC;AAAA,MACxB;AAAA,IACF,CAAC;AACD,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,mBAAmB,MAAM,UAAU,KAAK,IAAI,IAAI,YAAY,IAAI,MAAM;AAAA,MACxE;AAAA,IACF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,UAAU;AAC5B,YAAM,KAAK,WAAW,aAAa,mBAAmB,QAAQ,CAAC,KAAK;AACpE,aAAO;AAAA,QACL,yBAAyB,mBAAmB,IAAI,CAAC,GAAG,EAAE;AAAA,MACxD;AAAA,IACF;AAAA,IAEA,MAAM,YAAY,IAAI;AACpB,aAAO,QAAc,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,IAChE;AAAA,IAEA,MAAM,UAAU,WAAW,CAAC,GAAG;AAC7B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,SAAS,MAAM;AACzD,YAAM,KAAK,OAAO,SAAS;AAC3B,YAAM,WAAW,MAAM;AAAA,QACrB,gBAAgB,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,MACpC;AACA,UAAI,CAAC,SAAU,QAAO,CAAC;AACvB,YAAM,QAAQ,SAAS,QAAQ,CAAC;AAChC,UAAI,SAAS,UAAU;AACrB,eAAO,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,QAAQ;AAAA,MAC7D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response;\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n } catch (err) {\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const cause = err instanceof Error ? err.cause : undefined;\n const code =\n cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmBO,SAAS,aAAa,MAAsC;AACjE,QAAM,UAAU,KAAK,OAAO,QAAQ,OAAO,EAAE;AAC7C,QAAM,UAAU,KAAK,SAAS;AAE9B,QAAM,UAAU,OACd,MACA,SACsB;AACtB,UAAM,MAAM,GAAG,OAAO,GAAG,IAAI;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,YAAY,OAAoB;AAAA,MACpC,GAAG;AAAA,MACH,SAAS;AAAA,QACP,aAAa,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,GAAI,MAAM,WAAW,CAAC;AAAA,MACxB;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,QAAQ,KAAK,UAAU,CAAC;AAGpC,UAAI,IAAI,WAAW,KAAK;AACtB,cAAM,UAAU,SAAS,IAAI,QAAQ,IAAI,aAAa,KAAK,KAAK,EAAE;AAClE,cAAM,IAAI;AAAA,UAAQ,CAAC,YACjB,WAAW,SAAS,KAAK,IAAI,GAAG,OAAO,IAAI,GAAI;AAAA,QACjD;AACA,cAAM,MAAM,QAAQ,KAAK,UAAU,CAAC;AAAA,MACtC;AAAA,IACF,SAAS,KAAK;AAKZ,YAAM,QAAQ,eAAe,QAAQ,IAAI,QAAQ;AACjD,YAAM,OACJ,SAAS,OAAO,UAAU,YAAY,UAAU,QAC5C,OAAQ,MAA6B,IAAI,IACzC;AACN,YAAM,SAAS,OACX,GAAG,IAAI,GAAG,eAAe,SAAS,IAAI,UAAU,KAAK,IAAI,OAAO,MAAM,EAAE,KACxE,eAAe,QACb,IAAI,UACJ,OAAO,GAAG;AAChB,YAAM,IAAI,IAAI;AAAA,QACZ,mCAAmC,MAAM,IAAI,GAAG,WAAM,MAAM;AAAA,MAE9D;AACA,MAAC,EAAkC,QAAQ;AAC3C,YAAM;AAAA,IACR;AAEA,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,mBAAmB,MAAM,IAAI,GAAG,YAAY,IAAI,MAAM;AAAA,MACxD;AAAA,IACF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,UAAU;AAC5B,YAAM,KAAK,WAAW,aAAa,mBAAmB,QAAQ,CAAC,KAAK;AACpE,aAAO;AAAA,QACL,yBAAyB,mBAAmB,IAAI,CAAC,GAAG,EAAE;AAAA,MACxD;AAAA,IACF;AAAA,IAEA,MAAM,YAAY,IAAI;AACpB,aAAO,QAAc,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,IAChE;AAAA,IAEA,MAAM,UAAU,WAAW,CAAC,GAAG;AAC7B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,SAAS,MAAM;AACzD,YAAM,KAAK,OAAO,SAAS;AAC3B,YAAM,WAAW,MAAM;AAAA,QACrB,gBAAgB,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,MACpC;AACA,UAAI,CAAC,SAAU,QAAO,CAAC;AACvB,YAAM,QAAQ,SAAS,QAAQ,CAAC;AAChC,UAAI,SAAS,UAAU;AACrB,eAAO,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,QAAQ;AAAA,MAC7D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createClient
3
- } from "./chunk-TDHCPX4T.js";
3
+ } from "./chunk-OTLGKSYD.js";
4
4
  export {
5
5
  createClient
6
6
  };
@@ -30,7 +30,9 @@ function createClient(opts) {
30
30
  const baseUrl = opts.apiUrl.replace(/\/$/, "");
31
31
  const doFetch = opts.fetch ?? fetch;
32
32
  const request = async (path, init) => {
33
- const res = await doFetch(`${baseUrl}${path}`, {
33
+ const url = `${baseUrl}${path}`;
34
+ const method = init?.method ?? "GET";
35
+ const buildInit = () => ({
34
36
  ...init,
35
37
  headers: {
36
38
  "X-API-Key": opts.apiKey,
@@ -38,10 +40,30 @@ function createClient(opts) {
38
40
  ...init?.headers ?? {}
39
41
  }
40
42
  });
43
+ let res;
44
+ try {
45
+ res = await doFetch(url, buildInit());
46
+ if (res.status === 429) {
47
+ const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
48
+ await new Promise(
49
+ (resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
50
+ );
51
+ res = await doFetch(url, buildInit());
52
+ }
53
+ } catch (err) {
54
+ const cause = err instanceof Error ? err.cause : void 0;
55
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
56
+ const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
57
+ const e = new Error(
58
+ `chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
59
+ );
60
+ e.cause = err;
61
+ throw e;
62
+ }
41
63
  if (res.status === 404) return null;
42
64
  if (!res.ok) {
43
65
  throw new Error(
44
- `chaingrow-blog: ${init?.method ?? "GET"} ${path} failed (${res.status})`
66
+ `chaingrow-blog: ${method} ${url} failed (${res.status})`
45
67
  );
46
68
  }
47
69
  return await res.json();
@@ -74,14 +96,10 @@ function createClient(opts) {
74
96
  }
75
97
 
76
98
  // src/next/index.ts
99
+ var DEFAULT_API_URL = "https://api.chaingrow.de";
77
100
  var resolveEnv = (opts = {}) => {
78
- const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL;
101
+ const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL ?? DEFAULT_API_URL;
79
102
  const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;
80
- if (!apiUrl) {
81
- throw new Error(
82
- "chaingrow-blog: CHAINGROW_API_URL is not set (env var or apiUrl option required)"
83
- );
84
- }
85
103
  if (!apiKey) {
86
104
  throw new Error(
87
105
  "chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/index.ts","../../src/client.ts"],"sourcesContent":["import type { Blog } from '../types';\nimport { createClient } from '../client';\n\nexport type NextClientOptions = {\n apiUrl?: string;\n apiKey?: string;\n revalidate?: number | false;\n};\n\nconst resolveEnv = (opts: NextClientOptions = {}) => {\n const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL;\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n if (!apiUrl) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_URL is not set (env var or apiUrl option required)',\n );\n }\n if (!apiKey) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)',\n );\n }\n return { apiUrl, apiKey };\n};\n\nconst buildFetch = (tag: string, revalidate: number | false | undefined) => {\n return ((input: RequestInfo | URL, init?: RequestInit) =>\n fetch(input, {\n ...init,\n next: {\n tags: ['chaingrow:blog', tag],\n ...(revalidate === false\n ? { revalidate: false as const }\n : revalidate !== undefined\n ? { revalidate }\n : {}),\n },\n } as RequestInit)) as typeof fetch;\n};\n\nexport async function getBlog(\n slug: string,\n language?: string,\n opts: NextClientOptions = {},\n): Promise<Blog | null> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch(`chaingrow:blog:${slug}`, opts.revalidate),\n });\n return client.getBlog(slug, language);\n}\n\nexport async function listBlogs(\n listOpts: { status?: 'DRAFT' | 'PUBLISHED'; language?: string } = {},\n opts: NextClientOptions = {},\n): Promise<Blog[]> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch('chaingrow:blog:list', opts.revalidate),\n });\n return client.listBlogs(listOpts);\n}\n\nexport type { Blog } from '../types';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const res = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${init?.method ?? 'GET'} ${path} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/next/index.ts","../../src/client.ts"],"sourcesContent":["import type { Blog } from '../types';\nimport { createClient } from '../client';\n\nexport type NextClientOptions = {\n apiUrl?: string;\n apiKey?: string;\n revalidate?: number | false;\n};\n\nconst DEFAULT_API_URL = 'https://api.chaingrow.de';\n\nconst resolveEnv = (opts: NextClientOptions = {}) => {\n const apiUrl =\n opts.apiUrl ?? process.env.CHAINGROW_API_URL ?? DEFAULT_API_URL;\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)',\n );\n }\n return { apiUrl, apiKey };\n};\n\nconst buildFetch = (tag: string, revalidate: number | false | undefined) => {\n return ((input: RequestInfo | URL, init?: RequestInit) =>\n fetch(input, {\n ...init,\n next: {\n tags: ['chaingrow:blog', tag],\n ...(revalidate === false\n ? { revalidate: false as const }\n : revalidate !== undefined\n ? { revalidate }\n : {}),\n },\n } as RequestInit)) as typeof fetch;\n};\n\nexport async function getBlog(\n slug: string,\n language?: string,\n opts: NextClientOptions = {},\n): Promise<Blog | null> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch(`chaingrow:blog:${slug}`, opts.revalidate),\n });\n return client.getBlog(slug, language);\n}\n\nexport async function listBlogs(\n listOpts: { status?: 'DRAFT' | 'PUBLISHED'; language?: string } = {},\n opts: NextClientOptions = {},\n): Promise<Blog[]> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch('chaingrow:blog:list', opts.revalidate),\n });\n return client.listBlogs(listOpts);\n}\n\nexport type { Blog } from '../types';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response;\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n } catch (err) {\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const cause = err instanceof Error ? err.cause : undefined;\n const code =\n cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,16 +1,12 @@
1
1
  import {
2
2
  createClient
3
- } from "../chunk-TDHCPX4T.js";
3
+ } from "../chunk-OTLGKSYD.js";
4
4
 
5
5
  // src/next/index.ts
6
+ var DEFAULT_API_URL = "https://api.chaingrow.de";
6
7
  var resolveEnv = (opts = {}) => {
7
- const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL;
8
+ const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL ?? DEFAULT_API_URL;
8
9
  const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;
9
- if (!apiUrl) {
10
- throw new Error(
11
- "chaingrow-blog: CHAINGROW_API_URL is not set (env var or apiUrl option required)"
12
- );
13
- }
14
10
  if (!apiKey) {
15
11
  throw new Error(
16
12
  "chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["import type { Blog } from '../types';\nimport { createClient } from '../client';\n\nexport type NextClientOptions = {\n apiUrl?: string;\n apiKey?: string;\n revalidate?: number | false;\n};\n\nconst resolveEnv = (opts: NextClientOptions = {}) => {\n const apiUrl = opts.apiUrl ?? process.env.CHAINGROW_API_URL;\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n if (!apiUrl) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_URL is not set (env var or apiUrl option required)',\n );\n }\n if (!apiKey) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)',\n );\n }\n return { apiUrl, apiKey };\n};\n\nconst buildFetch = (tag: string, revalidate: number | false | undefined) => {\n return ((input: RequestInfo | URL, init?: RequestInit) =>\n fetch(input, {\n ...init,\n next: {\n tags: ['chaingrow:blog', tag],\n ...(revalidate === false\n ? { revalidate: false as const }\n : revalidate !== undefined\n ? { revalidate }\n : {}),\n },\n } as RequestInit)) as typeof fetch;\n};\n\nexport async function getBlog(\n slug: string,\n language?: string,\n opts: NextClientOptions = {},\n): Promise<Blog | null> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch(`chaingrow:blog:${slug}`, opts.revalidate),\n });\n return client.getBlog(slug, language);\n}\n\nexport async function listBlogs(\n listOpts: { status?: 'DRAFT' | 'PUBLISHED'; language?: string } = {},\n opts: NextClientOptions = {},\n): Promise<Blog[]> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch('chaingrow:blog:list', opts.revalidate),\n });\n return client.listBlogs(listOpts);\n}\n\nexport type { Blog } from '../types';\n"],"mappings":";;;;;AASA,IAAM,aAAa,CAAC,OAA0B,CAAC,MAAM;AACnD,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,IAAM,aAAa,CAAC,KAAa,eAA2C;AAC1E,UAAQ,CAAC,OAA0B,SACjC,MAAM,OAAO;AAAA,IACX,GAAG;AAAA,IACH,MAAM;AAAA,MACJ,MAAM,CAAC,kBAAkB,GAAG;AAAA,MAC5B,GAAI,eAAe,QACf,EAAE,YAAY,MAAe,IAC7B,eAAe,SACb,EAAE,WAAW,IACb,CAAC;AAAA,IACT;AAAA,EACF,CAAgB;AACpB;AAEA,eAAsB,QACpB,MACA,UACA,OAA0B,CAAC,GACL;AACtB,QAAM,EAAE,QAAQ,OAAO,IAAI,WAAW,IAAI;AAC1C,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,OAAO,WAAW,kBAAkB,IAAI,IAAI,KAAK,UAAU;AAAA,EAC7D,CAAC;AACD,SAAO,OAAO,QAAQ,MAAM,QAAQ;AACtC;AAEA,eAAsB,UACpB,WAAkE,CAAC,GACnE,OAA0B,CAAC,GACV;AACjB,QAAM,EAAE,QAAQ,OAAO,IAAI,WAAW,IAAI;AAC1C,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,OAAO,WAAW,uBAAuB,KAAK,UAAU;AAAA,EAC1D,CAAC;AACD,SAAO,OAAO,UAAU,QAAQ;AAClC;","names":[]}
1
+ {"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["import type { Blog } from '../types';\nimport { createClient } from '../client';\n\nexport type NextClientOptions = {\n apiUrl?: string;\n apiKey?: string;\n revalidate?: number | false;\n};\n\nconst DEFAULT_API_URL = 'https://api.chaingrow.de';\n\nconst resolveEnv = (opts: NextClientOptions = {}) => {\n const apiUrl =\n opts.apiUrl ?? process.env.CHAINGROW_API_URL ?? DEFAULT_API_URL;\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'chaingrow-blog: CHAINGROW_API_KEY is not set (env var or apiKey option required)',\n );\n }\n return { apiUrl, apiKey };\n};\n\nconst buildFetch = (tag: string, revalidate: number | false | undefined) => {\n return ((input: RequestInfo | URL, init?: RequestInit) =>\n fetch(input, {\n ...init,\n next: {\n tags: ['chaingrow:blog', tag],\n ...(revalidate === false\n ? { revalidate: false as const }\n : revalidate !== undefined\n ? { revalidate }\n : {}),\n },\n } as RequestInit)) as typeof fetch;\n};\n\nexport async function getBlog(\n slug: string,\n language?: string,\n opts: NextClientOptions = {},\n): Promise<Blog | null> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch(`chaingrow:blog:${slug}`, opts.revalidate),\n });\n return client.getBlog(slug, language);\n}\n\nexport async function listBlogs(\n listOpts: { status?: 'DRAFT' | 'PUBLISHED'; language?: string } = {},\n opts: NextClientOptions = {},\n): Promise<Blog[]> {\n const { apiUrl, apiKey } = resolveEnv(opts);\n const client = createClient({\n apiUrl,\n apiKey,\n fetch: buildFetch('chaingrow:blog:list', opts.revalidate),\n });\n return client.listBlogs(listOpts);\n}\n\nexport type { Blog } from '../types';\n"],"mappings":";;;;;AASA,IAAM,kBAAkB;AAExB,IAAM,aAAa,CAAC,OAA0B,CAAC,MAAM;AACnD,QAAM,SACJ,KAAK,UAAU,QAAQ,IAAI,qBAAqB;AAClD,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,IAAM,aAAa,CAAC,KAAa,eAA2C;AAC1E,UAAQ,CAAC,OAA0B,SACjC,MAAM,OAAO;AAAA,IACX,GAAG;AAAA,IACH,MAAM;AAAA,MACJ,MAAM,CAAC,kBAAkB,GAAG;AAAA,MAC5B,GAAI,eAAe,QACf,EAAE,YAAY,MAAe,IAC7B,eAAe,SACb,EAAE,WAAW,IACb,CAAC;AAAA,IACT;AAAA,EACF,CAAgB;AACpB;AAEA,eAAsB,QACpB,MACA,UACA,OAA0B,CAAC,GACL;AACtB,QAAM,EAAE,QAAQ,OAAO,IAAI,WAAW,IAAI;AAC1C,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,OAAO,WAAW,kBAAkB,IAAI,IAAI,KAAK,UAAU;AAAA,EAC7D,CAAC;AACD,SAAO,OAAO,QAAQ,MAAM,QAAQ;AACtC;AAEA,eAAsB,UACpB,WAAkE,CAAC,GACnE,OAA0B,CAAC,GACV;AACjB,QAAM,EAAE,QAAQ,OAAO,IAAI,WAAW,IAAI;AAC1C,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA,OAAO,WAAW,uBAAuB,KAAK,UAAU;AAAA,EAC1D,CAAC;AACD,SAAO,OAAO,UAAU,QAAQ;AAClC;","names":[]}
@@ -26,8 +26,9 @@ __export(webhook_exports, {
26
26
  module.exports = __toCommonJS(webhook_exports);
27
27
  var import_cache = require("next/cache");
28
28
  var import_node_crypto = require("crypto");
29
- var verifySignature = (body, signature, secret) => {
30
- const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
29
+ var deriveSigningKey = (apiKey) => (0, import_node_crypto.createHash)("sha256").update(apiKey).digest("hex");
30
+ var verifySignature = (body, signature, signingKey) => {
31
+ const expected = (0, import_node_crypto.createHmac)("sha256", signingKey).update(body).digest("hex");
31
32
  const provided = signature.trim();
32
33
  if (expected.length !== provided.length) return false;
33
34
  try {
@@ -42,11 +43,12 @@ var json = (status, body) => new Response(JSON.stringify(body), {
42
43
  });
43
44
  function createWebhookHandler(opts = {}) {
44
45
  const POST2 = async (req) => {
45
- const secret = opts.secret ?? process.env.CHAINGROW_WEBHOOK_SECRET;
46
+ const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;
46
47
  const body = await req.text();
47
- if (secret) {
48
+ if (apiKey) {
48
49
  const signature = req.headers.get("x-chaingrow-signature") ?? req.headers.get("X-ChainGrow-Signature") ?? "";
49
- if (!signature || !verifySignature(body, signature, secret)) {
50
+ const signingKey = deriveSigningKey(apiKey);
51
+ if (!signature || !verifySignature(body, signature, signingKey)) {
50
52
  return json(401, { error: "Invalid signature" });
51
53
  }
52
54
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/webhook.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookPayload } from '../types';\n\nexport type WebhookHandlerOptions = {\n secret?: string;\n onEvent?: (payload: WebhookPayload) => void | Promise<void>;\n};\n\nconst verifySignature = (body: string, signature: string, secret: string) => {\n const expected = createHmac('sha256', secret).update(body).digest('hex');\n const provided = signature.trim();\n if (expected.length !== provided.length) return false;\n try {\n return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));\n } catch {\n return false;\n }\n};\n\nconst json = (status: number, body: Record<string, unknown>) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n\nexport function createWebhookHandler(opts: WebhookHandlerOptions = {}) {\n const POST = async (req: Request): Promise<Response> => {\n const secret = opts.secret ?? process.env.CHAINGROW_WEBHOOK_SECRET;\n const body = await req.text();\n\n if (secret) {\n const signature =\n req.headers.get('x-chaingrow-signature') ??\n req.headers.get('X-ChainGrow-Signature') ??\n '';\n if (!signature || !verifySignature(body, signature, secret)) {\n return json(401, { error: 'Invalid signature' });\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = JSON.parse(body) as WebhookPayload;\n } catch {\n return json(400, { error: 'Invalid JSON body' });\n }\n\n const slug =\n typeof payload?.blog?.slug === 'string' ? payload.blog.slug : null;\n\n revalidateTag('chaingrow:blog');\n revalidateTag('chaingrow:blog:list');\n if (slug) revalidateTag(`chaingrow:blog:${slug}`);\n\n if (opts.onEvent) {\n try {\n await opts.onEvent(payload);\n } catch (err) {\n console.error('chaingrow-blog onEvent handler failed:', err);\n }\n }\n\n return json(200, { ok: true, revalidated: slug ?? null });\n };\n\n return { POST };\n}\n\nexport const { POST } = createWebhookHandler();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA8B;AAC9B,yBAA4C;AAQ5C,IAAM,kBAAkB,CAAC,MAAc,WAAmB,WAAmB;AAC3E,QAAM,eAAW,+BAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvE,QAAM,WAAW,UAAU,KAAK;AAChC,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI;AACF,eAAO,oCAAgB,OAAO,KAAK,QAAQ,GAAG,OAAO,KAAK,QAAQ,CAAC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAgB,SAC5B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,EACjC;AAAA,EACA,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;AAEI,SAAS,qBAAqB,OAA8B,CAAC,GAAG;AACrE,QAAMA,QAAO,OAAO,QAAoC;AACtD,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,QAAI,QAAQ;AACV,YAAM,YACJ,IAAI,QAAQ,IAAI,uBAAuB,KACvC,IAAI,QAAQ,IAAI,uBAAuB,KACvC;AACF,UAAI,CAAC,aAAa,CAAC,gBAAgB,MAAM,WAAW,MAAM,GAAG;AAC3D,eAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,aAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,IACjD;AAEA,UAAM,OACJ,OAAO,SAAS,MAAM,SAAS,WAAW,QAAQ,KAAK,OAAO;AAEhE,oCAAc,gBAAgB;AAC9B,oCAAc,qBAAqB;AACnC,QAAI,KAAM,iCAAc,kBAAkB,IAAI,EAAE;AAEhD,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,cAAM,KAAK,QAAQ,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,0CAA0C,GAAG;AAAA,MAC7D;AAAA,IACF;AAEA,WAAO,KAAK,KAAK,EAAE,IAAI,MAAM,aAAa,QAAQ,KAAK,CAAC;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAAA,MAAK;AAChB;AAEO,IAAM,EAAE,KAAK,IAAI,qBAAqB;","names":["POST"]}
1
+ {"version":3,"sources":["../../src/next/webhook.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport { createHash, createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookPayload } from '../types';\n\nexport type WebhookHandlerOptions = {\n apiKey?: string;\n onEvent?: (payload: WebhookPayload) => void | Promise<void>;\n};\n\n// Backend signs webhooks with sha256(apiKey) — the same hash it stores for auth.\n// We derive it here so customers only need CHAINGROW_API_KEY in their env.\nconst deriveSigningKey = (apiKey: string) =>\n createHash('sha256').update(apiKey).digest('hex');\n\nconst verifySignature = (\n body: string,\n signature: string,\n signingKey: string,\n) => {\n const expected = createHmac('sha256', signingKey).update(body).digest('hex');\n const provided = signature.trim();\n if (expected.length !== provided.length) return false;\n try {\n return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));\n } catch {\n return false;\n }\n};\n\nconst json = (status: number, body: Record<string, unknown>) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n\nexport function createWebhookHandler(opts: WebhookHandlerOptions = {}) {\n const POST = async (req: Request): Promise<Response> => {\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n const body = await req.text();\n\n if (apiKey) {\n const signature =\n req.headers.get('x-chaingrow-signature') ??\n req.headers.get('X-ChainGrow-Signature') ??\n '';\n const signingKey = deriveSigningKey(apiKey);\n if (!signature || !verifySignature(body, signature, signingKey)) {\n return json(401, { error: 'Invalid signature' });\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = JSON.parse(body) as WebhookPayload;\n } catch {\n return json(400, { error: 'Invalid JSON body' });\n }\n\n const slug =\n typeof payload?.blog?.slug === 'string' ? payload.blog.slug : null;\n\n revalidateTag('chaingrow:blog');\n revalidateTag('chaingrow:blog:list');\n if (slug) revalidateTag(`chaingrow:blog:${slug}`);\n\n if (opts.onEvent) {\n try {\n await opts.onEvent(payload);\n } catch (err) {\n console.error('chaingrow-blog onEvent handler failed:', err);\n }\n }\n\n return json(200, { ok: true, revalidated: slug ?? null });\n };\n\n return { POST };\n}\n\nexport const { POST } = createWebhookHandler();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA8B;AAC9B,yBAAwD;AAUxD,IAAM,mBAAmB,CAAC,eACxB,+BAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAElD,IAAM,kBAAkB,CACtB,MACA,WACA,eACG;AACH,QAAM,eAAW,+BAAW,UAAU,UAAU,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AAC3E,QAAM,WAAW,UAAU,KAAK;AAChC,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI;AACF,eAAO,oCAAgB,OAAO,KAAK,QAAQ,GAAG,OAAO,KAAK,QAAQ,CAAC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAgB,SAC5B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,EACjC;AAAA,EACA,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;AAEI,SAAS,qBAAqB,OAA8B,CAAC,GAAG;AACrE,QAAMA,QAAO,OAAO,QAAoC;AACtD,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,QAAI,QAAQ;AACV,YAAM,YACJ,IAAI,QAAQ,IAAI,uBAAuB,KACvC,IAAI,QAAQ,IAAI,uBAAuB,KACvC;AACF,YAAM,aAAa,iBAAiB,MAAM;AAC1C,UAAI,CAAC,aAAa,CAAC,gBAAgB,MAAM,WAAW,UAAU,GAAG;AAC/D,eAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,aAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,IACjD;AAEA,UAAM,OACJ,OAAO,SAAS,MAAM,SAAS,WAAW,QAAQ,KAAK,OAAO;AAEhE,oCAAc,gBAAgB;AAC9B,oCAAc,qBAAqB;AACnC,QAAI,KAAM,iCAAc,kBAAkB,IAAI,EAAE;AAEhD,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,cAAM,KAAK,QAAQ,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,0CAA0C,GAAG;AAAA,MAC7D;AAAA,IACF;AAEA,WAAO,KAAK,KAAK,EAAE,IAAI,MAAM,aAAa,QAAQ,KAAK,CAAC;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAAA,MAAK;AAChB;AAEO,IAAM,EAAE,KAAK,IAAI,qBAAqB;","names":["POST"]}
@@ -1,7 +1,7 @@
1
1
  import { b as WebhookPayload } from '../types-CoPD0ejB.cjs';
2
2
 
3
3
  type WebhookHandlerOptions = {
4
- secret?: string;
4
+ apiKey?: string;
5
5
  onEvent?: (payload: WebhookPayload) => void | Promise<void>;
6
6
  };
7
7
  declare function createWebhookHandler(opts?: WebhookHandlerOptions): {
@@ -1,7 +1,7 @@
1
1
  import { b as WebhookPayload } from '../types-CoPD0ejB.js';
2
2
 
3
3
  type WebhookHandlerOptions = {
4
- secret?: string;
4
+ apiKey?: string;
5
5
  onEvent?: (payload: WebhookPayload) => void | Promise<void>;
6
6
  };
7
7
  declare function createWebhookHandler(opts?: WebhookHandlerOptions): {
@@ -1,8 +1,9 @@
1
1
  // src/next/webhook.ts
2
2
  import { revalidateTag } from "next/cache";
3
- import { createHmac, timingSafeEqual } from "crypto";
4
- var verifySignature = (body, signature, secret) => {
5
- const expected = createHmac("sha256", secret).update(body).digest("hex");
3
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
4
+ var deriveSigningKey = (apiKey) => createHash("sha256").update(apiKey).digest("hex");
5
+ var verifySignature = (body, signature, signingKey) => {
6
+ const expected = createHmac("sha256", signingKey).update(body).digest("hex");
6
7
  const provided = signature.trim();
7
8
  if (expected.length !== provided.length) return false;
8
9
  try {
@@ -17,11 +18,12 @@ var json = (status, body) => new Response(JSON.stringify(body), {
17
18
  });
18
19
  function createWebhookHandler(opts = {}) {
19
20
  const POST2 = async (req) => {
20
- const secret = opts.secret ?? process.env.CHAINGROW_WEBHOOK_SECRET;
21
+ const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;
21
22
  const body = await req.text();
22
- if (secret) {
23
+ if (apiKey) {
23
24
  const signature = req.headers.get("x-chaingrow-signature") ?? req.headers.get("X-ChainGrow-Signature") ?? "";
24
- if (!signature || !verifySignature(body, signature, secret)) {
25
+ const signingKey = deriveSigningKey(apiKey);
26
+ if (!signature || !verifySignature(body, signature, signingKey)) {
25
27
  return json(401, { error: "Invalid signature" });
26
28
  }
27
29
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/webhook.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookPayload } from '../types';\n\nexport type WebhookHandlerOptions = {\n secret?: string;\n onEvent?: (payload: WebhookPayload) => void | Promise<void>;\n};\n\nconst verifySignature = (body: string, signature: string, secret: string) => {\n const expected = createHmac('sha256', secret).update(body).digest('hex');\n const provided = signature.trim();\n if (expected.length !== provided.length) return false;\n try {\n return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));\n } catch {\n return false;\n }\n};\n\nconst json = (status: number, body: Record<string, unknown>) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n\nexport function createWebhookHandler(opts: WebhookHandlerOptions = {}) {\n const POST = async (req: Request): Promise<Response> => {\n const secret = opts.secret ?? process.env.CHAINGROW_WEBHOOK_SECRET;\n const body = await req.text();\n\n if (secret) {\n const signature =\n req.headers.get('x-chaingrow-signature') ??\n req.headers.get('X-ChainGrow-Signature') ??\n '';\n if (!signature || !verifySignature(body, signature, secret)) {\n return json(401, { error: 'Invalid signature' });\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = JSON.parse(body) as WebhookPayload;\n } catch {\n return json(400, { error: 'Invalid JSON body' });\n }\n\n const slug =\n typeof payload?.blog?.slug === 'string' ? payload.blog.slug : null;\n\n revalidateTag('chaingrow:blog');\n revalidateTag('chaingrow:blog:list');\n if (slug) revalidateTag(`chaingrow:blog:${slug}`);\n\n if (opts.onEvent) {\n try {\n await opts.onEvent(payload);\n } catch (err) {\n console.error('chaingrow-blog onEvent handler failed:', err);\n }\n }\n\n return json(200, { ok: true, revalidated: slug ?? null });\n };\n\n return { POST };\n}\n\nexport const { POST } = createWebhookHandler();\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,SAAS,YAAY,uBAAuB;AAQ5C,IAAM,kBAAkB,CAAC,MAAc,WAAmB,WAAmB;AAC3E,QAAM,WAAW,WAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvE,QAAM,WAAW,UAAU,KAAK;AAChC,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI;AACF,WAAO,gBAAgB,OAAO,KAAK,QAAQ,GAAG,OAAO,KAAK,QAAQ,CAAC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAgB,SAC5B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,EACjC;AAAA,EACA,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;AAEI,SAAS,qBAAqB,OAA8B,CAAC,GAAG;AACrE,QAAMA,QAAO,OAAO,QAAoC;AACtD,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,QAAI,QAAQ;AACV,YAAM,YACJ,IAAI,QAAQ,IAAI,uBAAuB,KACvC,IAAI,QAAQ,IAAI,uBAAuB,KACvC;AACF,UAAI,CAAC,aAAa,CAAC,gBAAgB,MAAM,WAAW,MAAM,GAAG;AAC3D,eAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,aAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,IACjD;AAEA,UAAM,OACJ,OAAO,SAAS,MAAM,SAAS,WAAW,QAAQ,KAAK,OAAO;AAEhE,kBAAc,gBAAgB;AAC9B,kBAAc,qBAAqB;AACnC,QAAI,KAAM,eAAc,kBAAkB,IAAI,EAAE;AAEhD,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,cAAM,KAAK,QAAQ,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,0CAA0C,GAAG;AAAA,MAC7D;AAAA,IACF;AAEA,WAAO,KAAK,KAAK,EAAE,IAAI,MAAM,aAAa,QAAQ,KAAK,CAAC;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAAA,MAAK;AAChB;AAEO,IAAM,EAAE,KAAK,IAAI,qBAAqB;","names":["POST"]}
1
+ {"version":3,"sources":["../../src/next/webhook.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport { createHash, createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookPayload } from '../types';\n\nexport type WebhookHandlerOptions = {\n apiKey?: string;\n onEvent?: (payload: WebhookPayload) => void | Promise<void>;\n};\n\n// Backend signs webhooks with sha256(apiKey) — the same hash it stores for auth.\n// We derive it here so customers only need CHAINGROW_API_KEY in their env.\nconst deriveSigningKey = (apiKey: string) =>\n createHash('sha256').update(apiKey).digest('hex');\n\nconst verifySignature = (\n body: string,\n signature: string,\n signingKey: string,\n) => {\n const expected = createHmac('sha256', signingKey).update(body).digest('hex');\n const provided = signature.trim();\n if (expected.length !== provided.length) return false;\n try {\n return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));\n } catch {\n return false;\n }\n};\n\nconst json = (status: number, body: Record<string, unknown>) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n\nexport function createWebhookHandler(opts: WebhookHandlerOptions = {}) {\n const POST = async (req: Request): Promise<Response> => {\n const apiKey = opts.apiKey ?? process.env.CHAINGROW_API_KEY;\n const body = await req.text();\n\n if (apiKey) {\n const signature =\n req.headers.get('x-chaingrow-signature') ??\n req.headers.get('X-ChainGrow-Signature') ??\n '';\n const signingKey = deriveSigningKey(apiKey);\n if (!signature || !verifySignature(body, signature, signingKey)) {\n return json(401, { error: 'Invalid signature' });\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = JSON.parse(body) as WebhookPayload;\n } catch {\n return json(400, { error: 'Invalid JSON body' });\n }\n\n const slug =\n typeof payload?.blog?.slug === 'string' ? payload.blog.slug : null;\n\n revalidateTag('chaingrow:blog');\n revalidateTag('chaingrow:blog:list');\n if (slug) revalidateTag(`chaingrow:blog:${slug}`);\n\n if (opts.onEvent) {\n try {\n await opts.onEvent(payload);\n } catch (err) {\n console.error('chaingrow-blog onEvent handler failed:', err);\n }\n }\n\n return json(200, { ok: true, revalidated: slug ?? null });\n };\n\n return { POST };\n}\n\nexport const { POST } = createWebhookHandler();\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,SAAS,YAAY,YAAY,uBAAuB;AAUxD,IAAM,mBAAmB,CAAC,WACxB,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAElD,IAAM,kBAAkB,CACtB,MACA,WACA,eACG;AACH,QAAM,WAAW,WAAW,UAAU,UAAU,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AAC3E,QAAM,WAAW,UAAU,KAAK;AAChC,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI;AACF,WAAO,gBAAgB,OAAO,KAAK,QAAQ,GAAG,OAAO,KAAK,QAAQ,CAAC;AAAA,EACrE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAgB,SAC5B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,EACjC;AAAA,EACA,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;AAEI,SAAS,qBAAqB,OAA8B,CAAC,GAAG;AACrE,QAAMA,QAAO,OAAO,QAAoC;AACtD,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,QAAI,QAAQ;AACV,YAAM,YACJ,IAAI,QAAQ,IAAI,uBAAuB,KACvC,IAAI,QAAQ,IAAI,uBAAuB,KACvC;AACF,YAAM,aAAa,iBAAiB,MAAM;AAC1C,UAAI,CAAC,aAAa,CAAC,gBAAgB,MAAM,WAAW,UAAU,GAAG;AAC/D,eAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,MACjD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,aAAO,KAAK,KAAK,EAAE,OAAO,oBAAoB,CAAC;AAAA,IACjD;AAEA,UAAM,OACJ,OAAO,SAAS,MAAM,SAAS,WAAW,QAAQ,KAAK,OAAO;AAEhE,kBAAc,gBAAgB;AAC9B,kBAAc,qBAAqB;AACnC,QAAI,KAAM,eAAc,kBAAkB,IAAI,EAAE;AAEhD,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,cAAM,KAAK,QAAQ,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,0CAA0C,GAAG;AAAA,MAC7D;AAAA,IACF;AAEA,WAAO,KAAK,KAAK,EAAE,IAAI,MAAM,aAAa,QAAQ,KAAK,CAAC;AAAA,EAC1D;AAEA,SAAO,EAAE,MAAAA,MAAK;AAChB;AAEO,IAAM,EAAE,KAAK,IAAI,qBAAqB;","names":["POST"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaingrow-blog",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Drop-in Next.js SDK for rendering blogs published from ChainGrow.",
5
5
  "license": "MIT",
6
6
  "author": "ChainGrow",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const res = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${init?.method ?? 'GET'} ${path} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) return [];\n const blogs = response.data ?? [];\n if (listOpts.language) {\n return blogs.filter((b) => b.language === listOpts.language);\n }\n return blogs;\n },\n };\n}\n"],"mappings":";AAmBO,SAAS,aAAa,MAAsC;AACjE,QAAM,UAAU,KAAK,OAAO,QAAQ,OAAO,EAAE;AAC7C,QAAM,UAAU,KAAK,SAAS;AAE9B,QAAM,UAAU,OACd,MACA,SACsB;AACtB,UAAM,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,MAC7C,GAAG;AAAA,MACH,SAAS;AAAA,QACP,aAAa,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,GAAI,MAAM,WAAW,CAAC;AAAA,MACxB;AAAA,IACF,CAAC;AACD,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,mBAAmB,MAAM,UAAU,KAAK,IAAI,IAAI,YAAY,IAAI,MAAM;AAAA,MACxE;AAAA,IACF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,UAAU;AAC5B,YAAM,KAAK,WAAW,aAAa,mBAAmB,QAAQ,CAAC,KAAK;AACpE,aAAO;AAAA,QACL,yBAAyB,mBAAmB,IAAI,CAAC,GAAG,EAAE;AAAA,MACxD;AAAA,IACF;AAAA,IAEA,MAAM,YAAY,IAAI;AACpB,aAAO,QAAc,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,IAChE;AAAA,IAEA,MAAM,UAAU,WAAW,CAAC,GAAG;AAC7B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAS,OAAQ,QAAO,IAAI,UAAU,SAAS,MAAM;AACzD,YAAM,KAAK,OAAO,SAAS;AAC3B,YAAM,WAAW,MAAM;AAAA,QACrB,gBAAgB,KAAK,IAAI,EAAE,KAAK,EAAE;AAAA,MACpC;AACA,UAAI,CAAC,SAAU,QAAO,CAAC;AACvB,YAAM,QAAQ,SAAS,QAAQ,CAAC;AAChC,UAAI,SAAS,UAAU;AACrB,eAAO,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,QAAQ;AAAA,MAC7D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}