@monok/server 1.3.0 → 1.5.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/AGENTS.md ADDED
@@ -0,0 +1,442 @@
1
+ # Implementing `@monok/server` in a host application
2
+
3
+ This file is a self-contained implementation contract for AI coding assistants
4
+ and developers integrating Monok with an Express application or CMS. Follow the
5
+ requirements below rather than inventing alternate endpoint paths or response
6
+ shapes.
7
+
8
+ ## Choose the integration mode
9
+
10
+ Before changing the host application, establish which mode the developer wants:
11
+
12
+ 1. **Content only** — implement `createMonokRouter` and publication linkbacks.
13
+ 2. **Affiliate only** — implement `monokAffiliateAttribution` and
14
+ `createMonokReferral`; do not expose content endpoints.
15
+ 3. **Content and affiliate** — implement both independent contracts.
16
+
17
+ Do not assume affiliate functionality merely because the package supports it.
18
+ Updating an existing content-only installation requires no affiliate code or
19
+ configuration. Do not assume content delivery for an affiliate-only merchant.
20
+
21
+ ## Content integration
22
+
23
+ Follow this section only for **content only** or **content and affiliate** mode.
24
+ Expose a public HTTPS Monok content integration at these routes:
25
+
26
+ - `POST /api/monok/push`
27
+ - `GET /api/monok/info`
28
+ - `GET /api/monok/postfilter` when article discovery is supported
29
+
30
+ Use `createMonokRouter` from `@monok/server`. Mount it at `/api/monok` for the
31
+ default API base, or at an intentional custom base such as
32
+ `/something/specialAPI/monok`. When connecting the integration, enter either the
33
+ site origin to select the default or the complete custom base URL. Monok appends
34
+ `/info`, `/push`, and `/postfilter` to that base.
35
+
36
+ ## Content implementation
37
+
38
+ Install the package and Express:
39
+
40
+ ```bash
41
+ npm install @monok/server express
42
+ ```
43
+
44
+ Create one router with the same secret API key configured for the Monok
45
+ workspace:
46
+
47
+ ```ts
48
+ import express from "express";
49
+ import {
50
+ createMonokRouter,
51
+ sendMonokLinkback,
52
+ type MonokArticle,
53
+ type PostFilterQuery,
54
+ type PostFilterResult,
55
+ type SiteInfoInput
56
+ } from "@monok/server";
57
+
58
+ const app = express();
59
+
60
+ app.use(
61
+ "/api/monok",
62
+ createMonokRouter({
63
+ apiKey: process.env.MONOK_API_KEY!,
64
+ imageDir: "./uploads/monok",
65
+ getSiteInfo: async (): Promise<SiteInfoInput> => loadSiteInfo(),
66
+ searchArticles: async (
67
+ filters: PostFilterQuery
68
+ ): Promise<PostFilterResult[]> => searchPublishedArticles(filters),
69
+ onArticle: async (article, context) => {
70
+ await upsertMonokArticle(article, context);
71
+ }
72
+ })
73
+ );
74
+ ```
75
+
76
+ `onArticle` and the application's persistence work are always required.
77
+ `getSiteInfo` is optional. The router always exposes authenticated `/info`; when
78
+ the callback is omitted it returns minimal `Custom CMS` metadata so the standard
79
+ Monok dashboard connection and sync flow can still detect the installation.
80
+ `searchArticles` and `imageDir` are optional; omitting article search disables
81
+ covered-article and backlink discovery.
82
+
83
+ Do not add another JSON body parser solely for these routes. The router already
84
+ parses JSON with a 20 MB limit. A reverse proxy in front of the application must
85
+ also permit the request size.
86
+
87
+ ## Receiving and storing articles
88
+
89
+ `onArticle` receives:
90
+
91
+ ```ts
92
+ async function onArticle(
93
+ article: MonokArticle,
94
+ context: {
95
+ action?: "draft" | "publish";
96
+ saveThumbImage(fileNameBase?: string): Promise<string | undefined>;
97
+ }
98
+ ): Promise<void>
99
+ ```
100
+
101
+ The host must:
102
+
103
+ 1. Validate any application-specific constraints.
104
+ 2. Upsert by `article.id`, or enqueue an idempotent job keyed by `article.id`, so
105
+ repeated delivery cannot create duplicate content.
106
+ 3. Preserve `article.id` with the CMS record or queued job for later linkbacks
107
+ and updates.
108
+ 4. Store `article.content` as the rendered HTML body when processing the job.
109
+ 5. Apply the supplied slug, author, categories, sections, tags, language, and
110
+ CMS post type when the host supports them.
111
+ 6. Treat the requested action as draft unless `context.action === "publish"`.
112
+ 7. Before returning, await either durable CMS persistence or durable queue
113
+ acceptance. Do not acknowledge an in-memory, fire-and-forget task.
114
+ 8. Throw when persistence or durable queueing fails.
115
+
116
+ The draft/publish action normally arrives in the JSON body. The package also
117
+ supports the legacy query parameter. Do not read `article.action` directly when
118
+ deciding publication state; use the validated `context.action` value.
119
+
120
+ `onArticle` returns `void` or `Promise<void>`. It must not construct an HTTP
121
+ response. After the callback durably saves or queues the work, the package
122
+ returns:
123
+
124
+ ```json
125
+ {
126
+ "ok": true,
127
+ "status": "accepted",
128
+ "monokId": "the-article-id"
129
+ }
130
+ ```
131
+
132
+ This response acknowledges delivery only. It does not promise that a draft URL,
133
+ CMS record ID, thumbnail URL, or public canonical URL already exists. Custom CMS
134
+ implementations may render, moderate, deploy, or publish asynchronously.
135
+
136
+ Thrown errors become JSON error responses. An error with a numeric `status`
137
+ property uses that HTTP status; other errors return `500`.
138
+
139
+ ## Article payload
140
+
141
+ The important `MonokArticle` fields are:
142
+
143
+ ```ts
144
+ interface MonokArticle {
145
+ id: string | number;
146
+ title: string;
147
+ shortTitle?: string;
148
+ description?: string;
149
+ content?: string;
150
+ text?: string;
151
+ slug?: string;
152
+ action?: "draft" | "publish";
153
+ author?: { id: string | number; name: string };
154
+ categories?: Array<{ name: string; weight?: number }>;
155
+ sections?: string[];
156
+ tags?: string[] | null;
157
+ language?: string;
158
+ type?: string;
159
+ thumbnail?: string;
160
+ thumbImageData?: string;
161
+ thumbAlt?: string;
162
+ thumbImageDescription?: string;
163
+ keywords?: string[];
164
+ }
165
+ ```
166
+
167
+ Treat unknown additional fields as forward-compatible metadata. Do not reject a
168
+ payload merely because it contains fields added by a newer Monok version.
169
+
170
+ ## JPEG thumbnail handling
171
+
172
+ Monok sends `thumbImageData` as JPEG base64. It is normally raw base64, while a
173
+ legacy path can wrap it in `data:image/jpeg;base64,...`.
174
+
175
+ When `imageDir` is configured, call `await context.saveThumbImage()` to validate
176
+ and save it. The return value is the local `.jpg` path, or `undefined` when no
177
+ image directory or image data was provided. The default decoded-image limit is
178
+ 15 MiB and can be changed with `maxImageBytes`.
179
+
180
+ Saving a file is not the same as making it public. If the CMS has a media API or
181
+ object storage, the host may instead process `article.thumbImageData` itself.
182
+ Preserve `thumbAlt` or fall back to `thumbImageDescription`, then the title, for
183
+ accessible alt text.
184
+
185
+ Do not label the saved image as PNG and do not log its base64 data.
186
+
187
+ ## Site information
188
+
189
+ Implement `getSiteInfo` when Monok should offer real CMS authors, categories,
190
+ tags, post types, or languages. Return this shape:
191
+
192
+ ```ts
193
+ const info: SiteInfoInput = {
194
+ js_scripts: "Enabled",
195
+ name: "Site name",
196
+ logo: "https://example.com/logo.jpg",
197
+ categories: ["News", "Analysis"],
198
+ post_types: ["post", "page"],
199
+ tags: ["Technology", "Business"],
200
+ authors: [
201
+ {
202
+ id: "author-1",
203
+ name: "Jane Doe",
204
+ profile: "Editor",
205
+ avatar: "https://example.com/jane.jpg",
206
+ roles: ["editor"]
207
+ }
208
+ ],
209
+ languages: {
210
+ en: { name: "English", slug: "en", locale: "en_US" }
211
+ }
212
+ };
213
+ ```
214
+
215
+ Do not return `version` from `getSiteInfo`. The router always injects the
216
+ installed `@monok/server` package version into the `/info` response.
217
+
218
+ Return empty arrays when a supported collection has no entries. Values may be
219
+ loaded dynamically on every request. The router sends private no-cache headers.
220
+ If `getSiteInfo` is omitted, `/info` returns this authenticated fallback:
221
+
222
+ ```json
223
+ {
224
+ "js_scripts": "Disabled",
225
+ "version": "1.5.0",
226
+ "name": "Custom CMS",
227
+ "logo": "",
228
+ "categories": [],
229
+ "post_types": ["post"],
230
+ "tags": [],
231
+ "authors": []
232
+ }
233
+ ```
234
+
235
+ ## Article discovery with `/postfilter`
236
+
237
+ Implement `searchArticles` to expose published host content to Monok. The
238
+ callback receives normalized values:
239
+
240
+ ```ts
241
+ interface PostFilterQuery {
242
+ postType: string; // defaults to "post"
243
+ section?: string;
244
+ tag?: string;
245
+ query?: string;
246
+ amount: number; // defaults to 10
247
+ page: number; // defaults to 1
248
+ }
249
+ ```
250
+
251
+ Apply all supplied filters, return only published/public articles, and paginate
252
+ deterministically. Return an array using these exact snake-case keys:
253
+
254
+ ```ts
255
+ const result: PostFilterResult = {
256
+ canonical_url: "https://example.com/articles/example",
257
+ post_id: "cms-record-id",
258
+ title: "Example article",
259
+ description: "A short plain-text description"
260
+ };
261
+ ```
262
+
263
+ Return `[]` when nothing matches. `title` and `description` may be `null`.
264
+ Do not return drafts, private content, article bodies, credentials, or internal
265
+ metadata. If `searchArticles` is omitted, `/postfilter` returns `404`.
266
+
267
+ ## Authentication and cache behavior
268
+
269
+ The router performs authentication. The host should not duplicate it inside
270
+ callbacks.
271
+
272
+ - `/push` accepts Monok's `token` header, `X-API-Key`, or legacy `?apikey=`.
273
+ - `/info` and `/postfilter` accept `X-API-Key` or legacy `?apikey=`.
274
+ - Comparisons are timing-safe.
275
+ - `/info` and `/postfilter` responses include private no-cache headers.
276
+
277
+ Keep `MONOK_API_KEY` server-side. Never expose it to browser JavaScript, commit
278
+ it, include it in a URL shown to users, or log it. Prefer `X-API-Key` over query
279
+ authentication for custom calls because URLs are commonly logged by proxies.
280
+
281
+ ## Linkback after publication
282
+
283
+ When an article is actually published, notify Monok only after the public
284
+ canonical URL exists. This applies both to a draft published later and to an
285
+ asynchronous `action: "publish"` job:
286
+
287
+ The default destination is `https://app.monok.com/api/v1/linkback`.
288
+ `monokBaseUrl` is only an override for another Monok environment.
289
+
290
+ ```ts
291
+ await sendMonokLinkback({
292
+ monokId: storedMonokId,
293
+ externalId: cmsArticleId,
294
+ url: canonicalUrl,
295
+ title,
296
+ thumbnailUrl: publicThumbnailUrl
297
+ }, {
298
+ apiKey: process.env.MONOK_API_KEY
299
+ });
300
+ ```
301
+
302
+ Call this from the host's real publication lifecycle, not merely when `/push`
303
+ returns or when a job enters a queue. The linkback is the authoritative signal
304
+ that publication completed and a canonical URL exists. A retry should be safe.
305
+ When useful, `externalId` is the stable article ID assigned by the host CMS; it
306
+ is optional because the canonical URL also identifies the publication. When
307
+ supplied, `thumbnailUrl` is the public thumbnail URL.
308
+
309
+ ## Content verification checklist
310
+
311
+ Before declaring the integration complete, verify all applicable items:
312
+
313
+ - `GET /api/monok/info` returns `401` without a key.
314
+ - The same route succeeds with `X-API-Key` and with legacy `?apikey=`.
315
+ - Info responses contain `Cache-Control: no-store`.
316
+ - `POST /api/monok/push` accepts a body containing `action: "draft"`.
317
+ - A body containing `action: "publish"` is durably persisted or queued with the
318
+ requested publication intent.
319
+ - Successful pushes return `ok: true`, `status: "accepted"`, and the same
320
+ `monokId`, without requiring a canonical URL.
321
+ - Repeating the same `article.id` updates one record instead of duplicating it.
322
+ - Invalid action values return `400`.
323
+ - JPEG data saves as `.jpg`, and invalid or oversized image data is rejected.
324
+ - `/postfilter` applies search, type, section, tag, amount, and page filters.
325
+ - `/postfilter` returns exact `canonical_url`, `post_id`, `title`, and
326
+ `description` fields.
327
+ - Synchronous or asynchronous publication sends a linkback only after the public
328
+ canonical URL exists.
329
+ - The integration is reachable through its production HTTPS reverse proxy.
330
+
331
+ Do not claim completion based only on TypeScript compilation. Exercise the HTTP
332
+ routes and confirm one durable draft or draft job, one publication or publication
333
+ job, one repeated delivery, and one discovery query against the host
334
+ application's persistence layer. For asynchronous publishing, also complete one
335
+ job and verify its eventual linkback.
336
+
337
+ ## Affiliate integration
338
+
339
+ Follow this section only for **affiliate only** or **content and affiliate**
340
+ mode. A content-only installation must not be changed based on this section.
341
+
342
+ For Express, mount attribution before landing, signup, authentication, and
343
+ account-creation routes:
344
+
345
+ ```ts
346
+ import express from "express";
347
+ import {
348
+ createMonokReferral,
349
+ monokAffiliateAttribution
350
+ } from "@monok/server";
351
+
352
+ const app = express();
353
+
354
+ app.use(monokAffiliateAttribution());
355
+ app.use(express.json());
356
+ ```
357
+
358
+ The middleware reads the signed `monok_ref` query parameter, stores it in an
359
+ HttpOnly, Secure, SameSite=Lax cookie, and redirects to the same URL without the
360
+ token. The offer controls attribution duration; cookie expiry comes from the
361
+ token. Do not add a merchant-configured attribution duration. Only set
362
+ `secure: false` for local HTTP development.
363
+
364
+ For an apex landing site and an account application on a subdomain, explicitly
365
+ share the cookie across the parent domain:
366
+
367
+ ```ts
368
+ app.use(monokAffiliateAttribution({ cookieDomain: ".example.com" }));
369
+ ```
370
+
371
+ A cookie created on `example.com` with `Domain=.example.com` is sent to
372
+ `app.example.com`. Omit `cookieDomain` when landing and signup use the same
373
+ hostname. Avoid simultaneous host-only and parent-domain cookies with the same
374
+ name because they produce ambiguous duplicate cookies.
375
+
376
+ After durably creating a new customer, call Monok from that same successful
377
+ account-creation path:
378
+
379
+ ```ts
380
+ if (req.monokAttribution) {
381
+ await createMonokReferral(
382
+ {
383
+ attributionToken: req.monokAttribution,
384
+ customerReference: customer.id
385
+ },
386
+ { apiKey: process.env.MONOK_API_KEY! }
387
+ );
388
+ }
389
+ ```
390
+
391
+ Use a durable internal customer ID for `customerReference`, never an email or
392
+ other personal data. Do not register referrals during ordinary login,
393
+ invitation acceptance, or flows that do not create a new merchant customer.
394
+ Registration is idempotent for an offer/customer. A temporary Monok failure
395
+ must not roll back an already-created customer; retain the token and customer ID
396
+ in durable retry work. Keep `MONOK_API_KEY` server-side.
397
+
398
+ Monok's own sites use this same public package and API flow. Do not replace it
399
+ with direct database writes, an internal-only attribution service, or
400
+ `referredWorkspaceId` handling.
401
+
402
+ `monokAffiliateAttribution` is Express middleware;
403
+ `createMonokReferral` is framework-independent server code. In another server
404
+ framework, reproduce the capture contract server-side: read `monok_ref`, derive
405
+ expiry from the token, set the secure HttpOnly cookie, and redirect to the clean
406
+ URL. Then read the cookie during customer creation and pass it to
407
+ `createMonokReferral`. A static frontend cannot create an HttpOnly cookie; it
408
+ requires a server or CDN/edge capture handler. Client-side JavaScript cookies
409
+ are not an equivalent secure integration.
410
+
411
+ ### Configure the offer on app.monok.com
412
+
413
+ Briefly tell the developer to:
414
+
415
+ 1. Sign in to `app.monok.com` and select the merchant workspace.
416
+ 2. Open the relevant Brand and choose **Affiliate Program**.
417
+ 3. Create the offer with its exact HTTPS landing URL, commission terms,
418
+ duration, and attribution window.
419
+ 4. Deploy the integration, click **Test setup**, and confirm **Capture
420
+ verified**.
421
+ 5. Activate the offer when it is ready to receive attributed traffic.
422
+
423
+ The Affiliate Program page is currently development-only. If it is absent,
424
+ explain that it is not enabled in that environment. Do not invent another route
425
+ or substitute the content `/info` endpoint.
426
+
427
+ Affiliate integration does not require `createMonokRouter`, `/push`, `/info`,
428
+ or `/postfilter`.
429
+
430
+ ### Affiliate verification checklist
431
+
432
+ - Test the exact production landing URL through **Test setup**; do not fabricate
433
+ an attribution token locally.
434
+ - Confirm the offer becomes **Capture verified** before activation.
435
+ - For cross-subdomain flows, confirm the parent-domain cookie reaches the
436
+ account-creation hostname.
437
+ - Exercise each qualifying customer-creation path, including credentials and
438
+ OAuth where applicable, and confirm it calls `createMonokReferral`.
439
+ - Confirm login and invitation-only flows do not create referrals.
440
+ - Do not claim end-to-end verification yet. The dashboard currently validates
441
+ capture only; one-time test-customer callback validation is reserved for a
442
+ later implementation.
package/README.md CHANGED
@@ -3,7 +3,10 @@
3
3
  # `@monok/server`
4
4
 
5
5
  Monok server integration helpers for Node.js/Express.
6
- Easily connect your backend to Monok to receive articles, expose site info, and send linkbacks.
6
+ Connect a backend to Monok for content delivery, affiliate attribution, or both.
7
+
8
+ Using an AI coding assistant? Give it [`AGENTS.md`](./AGENTS.md). The guide contains
9
+ the complete host-implementation contract, response shapes, and verification checklist.
7
10
 
8
11
  ---
9
12
 
@@ -16,7 +19,7 @@ import { createMonokRouter, type MonokArticle } from "@monok/server";
16
19
  const app = express();
17
20
 
18
21
  app.use(
19
- "/monok",
22
+ "/api/monok",
20
23
  createMonokRouter({
21
24
  apiKey: process.env.MONOK_API_KEY!,
22
25
  onArticle: async (article: MonokArticle, { saveThumbImage, action }) => {
@@ -46,6 +49,52 @@ npm install @monok/server express
46
49
 
47
50
  ## 📚 Usage
48
51
 
52
+ ### Affiliate attribution (independent of content delivery)
53
+
54
+ The affiliate middleware preserves the signed `monok_ref` value from a Monok
55
+ redirect in an HttpOnly, SameSite=Lax cookie, then redirects to the same URL
56
+ without the token. The offer controls the attribution window and the middleware
57
+ derives the cookie expiry from the token, so merchants do not configure it.
58
+ Mount it before the merchant's landing and signup routes. After the customer has
59
+ been durably created, register the referral using the merchant's own stable
60
+ customer ID.
61
+
62
+ ```ts
63
+ import express from "express";
64
+ import {
65
+ createMonokReferral,
66
+ monokAffiliateAttribution
67
+ } from "@monok/server";
68
+
69
+ const app = express();
70
+ app.use(monokAffiliateAttribution());
71
+ app.use(express.json());
72
+
73
+ app.post("/signup", async (req, res) => {
74
+ const customer = await createCustomer(req.body);
75
+
76
+ if (req.monokAttribution) {
77
+ await createMonokReferral(
78
+ {
79
+ attributionToken: req.monokAttribution,
80
+ customerReference: customer.id
81
+ },
82
+ { apiKey: process.env.MONOK_API_KEY! }
83
+ );
84
+ }
85
+
86
+ res.status(201).json({ id: customer.id });
87
+ });
88
+ ```
89
+
90
+ The middleware defaults to secure cookies. For local HTTP development only, use
91
+ `monokAffiliateAttribution({ secure: false })`. `customerReference` should be a
92
+ durable internal ID, not an email address or other personal data. Repeating the
93
+ same offer/customer registration is idempotent and retains the first referral.
94
+
95
+ Affiliate-only merchants do **not** need to mount `createMonokRouter` or expose
96
+ `/push`, `/info`, or `/postfilter`.
97
+
49
98
  ### Express setup (TypeScript)
50
99
 
51
100
  ```ts
@@ -59,19 +108,21 @@ import {
59
108
  const app = express();
60
109
 
61
110
  app.use(
62
- "/monok",
111
+ "/api/monok",
63
112
  createMonokRouter({
64
113
  apiKey: process.env.MONOK_API_KEY!,
65
114
  imageDir: "./uploads", // optional
66
115
  // Optional: provide site info if you want Monok to query it
67
116
  getSiteInfo: {
68
117
  js_scripts: "Enabled",
69
- version: "1.0.0",
70
118
  name: "My Site",
71
119
  logo: "https://example.com/logo.png",
72
120
  categories: ["Content", "News Articles", "Uncategorized"],
73
121
  post_types: ["post", "page"],
74
122
  tags: ["Tech", "Health", "News"],
123
+ languages: {
124
+ en: { name: "English", slug: "en", locale: "en_US" }
125
+ },
75
126
  authors: [
76
127
  {
77
128
  name: "Jane Doe",
@@ -82,6 +133,15 @@ app.use(
82
133
  }
83
134
  ]
84
135
  },
136
+ searchArticles: async ({ postType, section, tag, query, amount, page }) => {
137
+ // Search published content in your CMS and return the requested page.
138
+ return [{
139
+ canonical_url: "https://example.com/blog/an-article",
140
+ post_id: "cms-123",
141
+ title: "An article",
142
+ description: "Article summary"
143
+ }];
144
+ },
85
145
  onArticle: async (article: MonokArticle, { saveThumbImage, action }) => {
86
146
  const imagePath = await saveThumbImage();
87
147
  console.log("Received article:", article.title);
@@ -95,9 +155,9 @@ app.use(
95
155
  // when you publish later, notify Monok:
96
156
  // await sendMonokLinkback({
97
157
  // monokId: article.id,
98
- // publicUrl: "https://example.com/blog/" + (article.slug ?? article.id),
158
+ // externalId: savedArticle.id,
159
+ // url: "https://example.com/blog/" + (article.slug ?? article.id),
99
160
  // title: article.title,
100
- // articleTextSample: article.text?.slice(0, 500),
101
161
  // thumbnailUrl: imagePath
102
162
  // ? "https://example.com/uploads/" + require("path").basename(imagePath)
103
163
  // : undefined
@@ -118,7 +178,7 @@ const { createMonokRouter } = require("@monok/server");
118
178
  const app = express();
119
179
 
120
180
  app.use(
121
- "/monok",
181
+ "/api/monok",
122
182
  createMonokRouter({
123
183
  apiKey: process.env.MONOK_API_KEY,
124
184
  onArticle: async (article, { saveThumbImage, action }) => {
@@ -141,29 +201,58 @@ app.listen(3000);
141
201
 
142
202
  ## 🔑 Endpoints
143
203
 
144
- * **`POST /monok/push[?action=draft|publish]`**
204
+ Mounting the router at `/api/monok` uses Monok's default API base. A custom or
205
+ versioned base is also supported, for example:
206
+
207
+ ```ts
208
+ app.use("/something/specialAPI/monok", createMonokRouter({ /* ... */ }));
209
+ ```
210
+
211
+ Enter that complete base URL when connecting the integration in Monok. Monok
212
+ appends `/info`, `/push`, and `/postfilter` to the configured base.
213
+
214
+ * **`POST /api/monok/push`**
145
215
  Monok pushes new article data here.
146
216
  Header: `token: <your-api-key>`
147
217
  Body: JSON payload of the article.
148
218
 
149
- * `action` query param may be `draft` or `publish` depending on GUI dropdown selection.
219
+ * `action` is read from the JSON body and may be `draft` or `publish`.
220
+ * A legacy `?action=draft|publish` query parameter is also accepted.
221
+ * After `onArticle` durably saves or queues the work, the route acknowledges
222
+ it with `{ "ok": true, "status": "accepted", "monokId": "..." }`. This
223
+ does not claim that asynchronous publication has completed.
224
+
225
+ * **`GET /api/monok/info`**
226
+ Monok fetches this to authenticate and learn your site branding, categories, tags, and authors.
227
+ If `getSiteInfo` is not provided, the route remains available and returns a
228
+ minimal `Custom CMS` response with empty metadata collections.
150
229
 
151
- * **`GET /monok/info?apikey=<your-api-key>`**
152
- If `getSiteInfo` is configured, Monok fetches this to learn your site branding, categories, tags, and authors.
153
- If `getSiteInfo` is not provided, this route responds with `404`.
230
+ * **`GET /api/monok/postfilter`**
231
+ If `searchArticles` is configured, Monok uses this route to search published
232
+ content. Supported parameters are `post_type`, `section`, `tag`, `q`, `amount`,
233
+ and `page`. If it is not configured, the route responds with `404`.
234
+
235
+ `GET /info` and `GET /postfilter` accept the API key in the `X-API-Key` header
236
+ (preferred) or the legacy `?apikey=` query parameter. Their responses include
237
+ private no-cache headers. `POST /push` also accepts Monok's `token` header.
154
238
 
155
239
  ---
156
240
 
157
241
  ## 🔗 Linkback after publishing
158
242
 
243
+ The push acknowledgement confirms delivery only. If publication is asynchronous,
244
+ send the linkback later, after the public canonical URL actually exists.
245
+ Linkbacks default to `https://app.monok.com/api/v1/linkback`; use
246
+ `monokBaseUrl` only when targeting another Monok environment.
247
+
159
248
  ```ts
160
249
  import { sendMonokLinkback } from "@monok/server";
161
250
 
162
251
  await sendMonokLinkback({
163
252
  monokId: article.id,
164
- publicUrl: "https://example.com/blog/" + (article.slug ?? article.id),
253
+ externalId: cmsArticle.id,
254
+ url: "https://example.com/blog/" + (article.slug ?? article.id),
165
255
  title: article.title,
166
- articleTextSample: article.text?.slice(0, 500),
167
256
  thumbnailUrl: "https://example.com/uploads/" + imageFile
168
257
  });
169
258
  ```
@@ -176,8 +265,10 @@ await sendMonokLinkback({
176
265
  | ------------- | ----------------------------------------- | -------- | --------------------------------------------------------------------------------- |
177
266
  | `apiKey` | `string` | ✅ | Token Monok sends in headers / query. |
178
267
  | `onArticle` | `(article, ctx) => void \| Promise<void>` | ✅ | Handle new article pushes. `ctx` includes `saveThumbImage` and optional `action`. |
179
- | `getSiteInfo` | `SiteInfo \| () => SiteInfo` | ❌ | Return site info in the Monok format. If omitted, `GET /info` responds 404. |
180
- | `imageDir` | `string` | ❌ | Directory to save `thumbImageData`. |
268
+ | `getSiteInfo` | `SiteInfoInput \| () => SiteInfoInput` | ❌ | Return host metadata; the router injects its package version. A minimal fallback is used when omitted. |
269
+ | `searchArticles` | `(query) => PostFilterResult[]` | ❌ | Enables authenticated article discovery through `GET /postfilter`. |
270
+ | `imageDir` | `string` | ❌ | Directory in which validated JPEG thumbnails are saved. |
271
+ | `maxImageBytes` | `number` | ❌ | Maximum decoded JPEG size; defaults to 15 MiB. |
181
272
  | `basePath` | `string` | ❌ | Custom prefix for routes (default ""). |
182
273
 
183
274
  ---
@@ -0,0 +1,15 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { MonokAffiliateAttributionOptions } from "./types.js";
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ monokAttribution?: string;
7
+ }
8
+ }
9
+ }
10
+ /**
11
+ * Preserve Monok's signed redirect token in an HttpOnly cookie and expose it as
12
+ * `req.monokAttribution`. This does not expose any content-receiving endpoint.
13
+ */
14
+ export declare function monokAffiliateAttribution(options?: MonokAffiliateAttributionOptions): RequestHandler;
15
+ //# sourceMappingURL=affiliate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affiliate.d.ts","sourceRoot":"","sources":["../src/affiliate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAC;AAcnE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;SAC3B;KACF;CACF;AAgED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,cAAc,CAuChB"}