@soloworks/smking-next 0.20.3 → 0.21.1
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/CHANGELOG.md +40 -0
- package/package.json +1 -1
- package/src/cms-blocks.ts +219 -0
- package/src/components/smking-runtime.tsx +5 -0
- package/src/index.ts +18 -0
- package/src/lib/webhook-route.ts +53 -0
- package/src/types.ts +27 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @soloworks/smking-next
|
|
2
2
|
|
|
3
|
+
## 0.21.1 — 2026-06-12
|
|
4
|
+
|
|
5
|
+
**Webhook replay protection (security review M6).**
|
|
6
|
+
|
|
7
|
+
The webhook receiver now rejects deliveries whose `deliveredAt` falls
|
|
8
|
+
outside a ±5-minute window (401 `stale_delivery`; a missing field counts
|
|
9
|
+
as stale — the SaaS has sent it since v0.11) and dedupes `deliveryId`
|
|
10
|
+
re-sends inside that window (401 `duplicate_delivery`). Dedup is
|
|
11
|
+
best-effort in-memory per serverless instance; the time window is the
|
|
12
|
+
primary defense. The SaaS now stamps every delivery with a unique
|
|
13
|
+
`deliveryId` — payloads from a SaaS predating the field skip dedup and
|
|
14
|
+
keep working. Both fields sit inside the HMAC-signed JSON body, so an
|
|
15
|
+
attacker can't forge or strip them. No customer action needed; `^0.21`
|
|
16
|
+
picks this up automatically.
|
|
17
|
+
|
|
18
|
+
**Mode B block types caught up with the SaaS catalog (types only — no
|
|
19
|
+
runtime change).**
|
|
20
|
+
|
|
21
|
+
The `Block` union had drifted to 3 of the SaaS's 10 block components; it
|
|
22
|
+
now covers all of them — added `search`, `nav-recent-posts`,
|
|
23
|
+
`nav-related-posts`, `nav-category-index`, `image`, `slideshow`, and
|
|
24
|
+
`social-share` (plus the missing `NavSnapshotEntry.href` field). The types
|
|
25
|
+
are no longer a hand-maintained mirror: the monorepo's `@smking/shared`
|
|
26
|
+
package is the single source of truth and `src/cms-blocks.ts` is a
|
|
27
|
+
generated vendored copy (`pnpm sync:block-types`, guarded by a vitest
|
|
28
|
+
sync test). All block prop interfaces are also exported from the package
|
|
29
|
+
root now (`Block`, `BlockComponent`, `SearchProps`, …). Mode A
|
|
30
|
+
(`bodyHtml`) rendering is unaffected.
|
|
31
|
+
|
|
32
|
+
## 0.21.0 — 2026-06-09
|
|
33
|
+
|
|
34
|
+
**Per-site CMS theme — `<SmkingRuntime>` now mounts a `theme.css` stylesheet.**
|
|
35
|
+
|
|
36
|
+
`SmkingRuntime` gains an optional `apiKey` prop (auto-resolved from
|
|
37
|
+
`SMKING_API_KEY`); when set, it injects `<link rel="stylesheet"
|
|
38
|
+
href="…/api/v1/public/theme.css?key=…">` right after `runtime.css` so the
|
|
39
|
+
site's shadcn theme variables override the neutral defaults inside the
|
|
40
|
+
`[data-smking="cms"]` container. No customer code change — the env var is
|
|
41
|
+
already required; the link is omitted when no key is configured.
|
|
42
|
+
|
|
3
43
|
## 0.20.3 — 2026-06-08
|
|
4
44
|
|
|
5
45
|
**CMS draft preview URL now carries a visible `?smking_preview=1` marker.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soloworks/smking-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "AI-native SEO (AEO) for Next.js — auto-inject JSON-LD, FAQ, AI summary, and SEO metadata so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your pages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/sillyleo/smking/tree/main/packages/smking-next",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Vendored copy of packages/shared/src/types/cms-blocks.ts (the SDK ships
|
|
3
|
+
// raw src/ to npm, so it can't import the private workspace package).
|
|
4
|
+
// Regenerate with: pnpm sync:block-types
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CMS block primitives — substrate-pivot v2.
|
|
8
|
+
*
|
|
9
|
+
* Single source of truth for the Mode B (blocks JSON) contract shared by
|
|
10
|
+
* the SaaS dashboard (`apps/web/src/features/cms`) and the customer SDKs.
|
|
11
|
+
* The dashboard editor serializes Plate documents into this shape, the
|
|
12
|
+
* publish pipeline materializes nav snapshots into it, and
|
|
13
|
+
* `/api/v1/public/page` returns it verbatim.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ `@soloworks/smking-next` publishes raw `src/` to npm and cannot import
|
|
16
|
+
* this private workspace package — it ships a vendored copy at
|
|
17
|
+
* `packages/smking-next/src/cms-blocks.ts` instead. After editing this file
|
|
18
|
+
* run `pnpm sync:block-types` to regenerate it (drift fails the SDK's
|
|
19
|
+
* vitest guard).
|
|
20
|
+
*
|
|
21
|
+
* `article` is the serialize-time HTML container for runs of Plate built-in
|
|
22
|
+
* blocks — paragraphs / headings / lists are collapsed into one HTML body
|
|
23
|
+
* block via Plate's `serializeHtml`. New block primitives land here + in
|
|
24
|
+
* apps/web `block-schemas.ts`.
|
|
25
|
+
*
|
|
26
|
+
* `nav-taxonomy-list` uses a `source` discriminator — category mode is
|
|
27
|
+
* slug-prefix derived, tag mode targets `taxonomies.slug`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface HeroProps {
|
|
31
|
+
title: string;
|
|
32
|
+
subtitle?: string;
|
|
33
|
+
image?: { url: string; alt: string };
|
|
34
|
+
cta?: { label: string; href: string };
|
|
35
|
+
/** Header layout — omitted = the original centred hero; "start" = the
|
|
36
|
+
* Apple-newsroom-style left-aligned header; "cover" = title-only banner
|
|
37
|
+
* over a full-bleed background image (Apple services index). Visuals ship
|
|
38
|
+
* via bodyHtml; this mirrors the node attr for SDK Mode B (per-block)
|
|
39
|
+
* consumers. */
|
|
40
|
+
align?: "center" | "start" | "cover";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ArticleProps {
|
|
44
|
+
/** Plate `serializeHtml` output — a contiguous run of built-in blocks
|
|
45
|
+
* (paragraphs / headings / lists) collapsed into one HTML body block. */
|
|
46
|
+
html: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ModuleHeader {
|
|
50
|
+
heading?: string;
|
|
51
|
+
viewAll?: { label: string; href: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type NavLayout = "grid" | "list" | "carousel" | "archive";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Materialized snapshot entry written into nav-* block props at publish
|
|
58
|
+
* time (see apps/web `materialize-blocks.ts` for the resolver). The
|
|
59
|
+
* customer SDK echoes `snapshot` verbatim — it never round-trips back to
|
|
60
|
+
* smking for nav rendering, so if smking is down customer pages still
|
|
61
|
+
* render whatever was last published.
|
|
62
|
+
*/
|
|
63
|
+
export interface NavSnapshotEntry {
|
|
64
|
+
slug: string;
|
|
65
|
+
/**
|
|
66
|
+
* Absolute href = the customer's CMS mount prefix + slug (baked at publish
|
|
67
|
+
* from `sites.config.cmsBasePath`). Optional — falls back to `/${slug}`.
|
|
68
|
+
*/
|
|
69
|
+
href?: string;
|
|
70
|
+
title: string | null;
|
|
71
|
+
excerpt: string | null;
|
|
72
|
+
featuredImageUrl: string | null;
|
|
73
|
+
/** ISO string. */
|
|
74
|
+
publishedAt: string | null;
|
|
75
|
+
contentType: "article" | "landing" | "listing" | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `search` block — a command-palette over the WHOLE site's published CMS
|
|
80
|
+
* pages. Unlike `nav-taxonomy-list` (which scopes to a category / tag),
|
|
81
|
+
* search bakes EVERY published `cms_page` into `snapshot` at publish time
|
|
82
|
+
* and the `<smking-search>` web component filters the rendered cards
|
|
83
|
+
* client-side. Reuses `NavSnapshotEntry` (the static render only reads
|
|
84
|
+
* `href` / `title` / `excerpt` — no thumbnail, command-palette text feel).
|
|
85
|
+
*/
|
|
86
|
+
export interface SearchProps {
|
|
87
|
+
/** Input placeholder. Defaults to "Search…" at render time. */
|
|
88
|
+
placeholder?: string;
|
|
89
|
+
/** Optional heading above the search input. */
|
|
90
|
+
heading?: string;
|
|
91
|
+
/** Publish-time materialized snapshot — every published page on the site. */
|
|
92
|
+
snapshot?: NavSnapshotEntry[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RecentPostsProps extends ModuleHeader {
|
|
96
|
+
/** Author-set number of latest posts to show (1..50). */
|
|
97
|
+
limit: number;
|
|
98
|
+
/** Publish-time materialized snapshot — whole-site newest-first. */
|
|
99
|
+
snapshot?: NavSnapshotEntry[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface RelatedPostsProps extends ModuleHeader {
|
|
103
|
+
/** Author-set number of related posts to show (1..50). */
|
|
104
|
+
limit: number;
|
|
105
|
+
/** Publish-time materialized snapshot — same-tag, ranked by overlap. */
|
|
106
|
+
snapshot?: NavSnapshotEntry[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One author-selected category in a nav-category-index block. */
|
|
110
|
+
export interface CategoryIndexItem {
|
|
111
|
+
/** Slug prefix identifying the category (e.g. `blog/seo`). */
|
|
112
|
+
path: string;
|
|
113
|
+
/** Display name on the card (slug leaf by default; author-editable). */
|
|
114
|
+
label: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Materialized card for nav-category-index: the category's name, its archive
|
|
119
|
+
* link, and its latest post (null when the category has no published posts).
|
|
120
|
+
*/
|
|
121
|
+
export interface CategoryCardSnapshot {
|
|
122
|
+
label: string;
|
|
123
|
+
/** Archive href = mount prefix + category path (baked at publish). */
|
|
124
|
+
href: string;
|
|
125
|
+
latest: NavSnapshotEntry | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CategoryIndexProps extends ModuleHeader {
|
|
129
|
+
items: CategoryIndexItem[];
|
|
130
|
+
/** Card layout — omitted/"cards" = category cards (archive links);
|
|
131
|
+
* "featured" = Apple-newsroom services-index tiles where each card IS the
|
|
132
|
+
* category's latest article. Visuals ship via bodyHtml; this mirrors the
|
|
133
|
+
* node attr for SDK Mode B (per-block) consumers. */
|
|
134
|
+
layout?: "cards" | "featured";
|
|
135
|
+
snapshot?: CategoryCardSnapshot[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `image` block — a single figure (image + optional caption). `widthMode`
|
|
140
|
+
* controls the breakout (content = text column, wide = up to 1024px centred
|
|
141
|
+
* on the container). All fields optional so an unconfigured block is valid;
|
|
142
|
+
* the static render emits nothing until an image is uploaded.
|
|
143
|
+
*/
|
|
144
|
+
export interface ImageProps {
|
|
145
|
+
url?: string;
|
|
146
|
+
alt?: string;
|
|
147
|
+
caption?: string;
|
|
148
|
+
widthMode?: "content" | "wide";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One slide in a `slideshow` block. */
|
|
152
|
+
export interface SlideshowSlideProps {
|
|
153
|
+
url: string;
|
|
154
|
+
alt?: string;
|
|
155
|
+
caption?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* `slideshow` block — an image carousel. `widthMode` controls the breakout
|
|
160
|
+
* (content / wide). `slides` optional so an unconfigured block is valid; the
|
|
161
|
+
* static render emits nothing (and the customer SDK upgrades it into a
|
|
162
|
+
* paginated carousel via the `<smking-slideshow>` web component).
|
|
163
|
+
*/
|
|
164
|
+
export interface SlideshowProps {
|
|
165
|
+
slides?: SlideshowSlideProps[];
|
|
166
|
+
widthMode?: "content" | "wide";
|
|
167
|
+
/** Fixed crop ratio for every slide (render default 16:9). */
|
|
168
|
+
aspectRatio?: "16:9" | "4:3";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `social-share` block — X / Facebook / LinkedIn share links + copy-link. The
|
|
173
|
+
* platforms are fixed; `shareUrl` is the optional no-JS fallback target (the
|
|
174
|
+
* `<smking-share>` web component overrides every link from window.location at
|
|
175
|
+
* runtime, so the live share always points at the real current page).
|
|
176
|
+
*/
|
|
177
|
+
export interface SocialShareProps {
|
|
178
|
+
shareUrl?: string;
|
|
179
|
+
/** Heading above the row. Omitted = "Share article" default at render
|
|
180
|
+
* time; empty string = bare icon row (Apple-newsroom header style). */
|
|
181
|
+
label?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface NavTaxonomyListProps extends ModuleHeader {
|
|
185
|
+
/**
|
|
186
|
+
* Discriminator:
|
|
187
|
+
* - `category-by-path` → `path` is a slug prefix (e.g. `/blog/seo`); SDK
|
|
188
|
+
* queries pages whose slug starts with this prefix.
|
|
189
|
+
* - `tag` → `path` is the taxonomy slug (flat tag namespace).
|
|
190
|
+
*/
|
|
191
|
+
source: "category-by-path" | "tag";
|
|
192
|
+
path: string;
|
|
193
|
+
limit: number;
|
|
194
|
+
/** List EVERY published article under the category/tag instead of the
|
|
195
|
+
* latest `limit` (Apple archive page). The resolver still applies a 200
|
|
196
|
+
* safety cap. Omitted (not false) when off — back-compat shape. */
|
|
197
|
+
showAll?: boolean;
|
|
198
|
+
layout: NavLayout;
|
|
199
|
+
/** Publish-time materialized snapshot. */
|
|
200
|
+
snapshot?: NavSnapshotEntry[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type Block =
|
|
204
|
+
| { component: "hero"; id: string; props: HeroProps }
|
|
205
|
+
| { component: "article"; id: string; props: ArticleProps }
|
|
206
|
+
| {
|
|
207
|
+
component: "nav-taxonomy-list";
|
|
208
|
+
id: string;
|
|
209
|
+
props: NavTaxonomyListProps;
|
|
210
|
+
}
|
|
211
|
+
| { component: "search"; id: string; props: SearchProps }
|
|
212
|
+
| { component: "nav-recent-posts"; id: string; props: RecentPostsProps }
|
|
213
|
+
| { component: "nav-related-posts"; id: string; props: RelatedPostsProps }
|
|
214
|
+
| { component: "nav-category-index"; id: string; props: CategoryIndexProps }
|
|
215
|
+
| { component: "image"; id: string; props: ImageProps }
|
|
216
|
+
| { component: "slideshow"; id: string; props: SlideshowProps }
|
|
217
|
+
| { component: "social-share"; id: string; props: SocialShareProps };
|
|
218
|
+
|
|
219
|
+
export type BlockComponent = Block["component"];
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export function SmkingRuntime({
|
|
17
17
|
baseUrl,
|
|
18
|
+
apiKey,
|
|
18
19
|
}: {
|
|
19
20
|
baseUrl?: string;
|
|
21
|
+
apiKey?: string;
|
|
20
22
|
}) {
|
|
21
23
|
const url = (
|
|
22
24
|
baseUrl ?? process.env.SMKING_BASE_URL ?? "https://smking.app"
|
|
@@ -24,6 +26,9 @@ export function SmkingRuntime({
|
|
|
24
26
|
return (
|
|
25
27
|
<>
|
|
26
28
|
<link rel="stylesheet" href={`${url}/api/v1/public/runtime.css`} />
|
|
29
|
+
{apiKey && (
|
|
30
|
+
<link rel="stylesheet" href={`${url}/api/v1/public/theme.css?key=${apiKey}`} />
|
|
31
|
+
)}
|
|
27
32
|
<script src={`${url}/api/v1/public/runtime.js`} async />
|
|
28
33
|
</>
|
|
29
34
|
);
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,12 @@ export { getCmsPage } from "./lib/cms-client";
|
|
|
6
6
|
export type {
|
|
7
7
|
AeoResponse,
|
|
8
8
|
AeoStatus,
|
|
9
|
+
ArticleProps,
|
|
10
|
+
Block,
|
|
11
|
+
BlockComponent,
|
|
12
|
+
CategoryCardSnapshot,
|
|
13
|
+
CategoryIndexItem,
|
|
14
|
+
CategoryIndexProps,
|
|
9
15
|
ChatLinks,
|
|
10
16
|
CmsPage,
|
|
11
17
|
CmsParams,
|
|
@@ -13,5 +19,17 @@ export type {
|
|
|
13
19
|
CmsStatus,
|
|
14
20
|
DiscoverParams,
|
|
15
21
|
FaqItem,
|
|
22
|
+
HeroProps,
|
|
23
|
+
ImageProps,
|
|
24
|
+
ModuleHeader,
|
|
25
|
+
NavLayout,
|
|
26
|
+
NavSnapshotEntry,
|
|
27
|
+
NavTaxonomyListProps,
|
|
28
|
+
RecentPostsProps,
|
|
29
|
+
RelatedPostsProps,
|
|
30
|
+
SearchProps,
|
|
16
31
|
SeoMeta,
|
|
32
|
+
SlideshowProps,
|
|
33
|
+
SlideshowSlideProps,
|
|
34
|
+
SocialShareProps,
|
|
17
35
|
} from "./types";
|
package/src/lib/webhook-route.ts
CHANGED
|
@@ -7,6 +7,35 @@ interface WebhookPayload {
|
|
|
7
7
|
paths?: string[];
|
|
8
8
|
slugs?: string[];
|
|
9
9
|
deliveredAt?: string;
|
|
10
|
+
deliveryId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Replay protection (v0.21.1+). The deliveredAt window is the primary
|
|
14
|
+
// defense — ±5min absorbs clock skew between SaaS and customer while
|
|
15
|
+
// capping how long an intercepted delivery stays replayable. deliveryId
|
|
16
|
+
// dedup is best-effort on top: serverless instances each hold their own
|
|
17
|
+
// Map, so a replay landing on a different instance only has the window
|
|
18
|
+
// to beat. SaaS payloads predating deliveryId pass dedup untouched.
|
|
19
|
+
const REPLAY_WINDOW_MS = 5 * 60 * 1000;
|
|
20
|
+
const SEEN_DELIVERIES_MAX = 1000;
|
|
21
|
+
const seenDeliveries = new Map<string, number>(); // deliveryId → expiry (ms)
|
|
22
|
+
|
|
23
|
+
function isDuplicateDelivery(
|
|
24
|
+
deliveryId: string | undefined,
|
|
25
|
+
now: number,
|
|
26
|
+
): boolean {
|
|
27
|
+
for (const [id, expiry] of seenDeliveries) {
|
|
28
|
+
if (expiry <= now) seenDeliveries.delete(id);
|
|
29
|
+
}
|
|
30
|
+
if (typeof deliveryId !== "string" || deliveryId.length === 0) return false;
|
|
31
|
+
if (seenDeliveries.has(deliveryId)) return true;
|
|
32
|
+
if (seenDeliveries.size >= SEEN_DELIVERIES_MAX) {
|
|
33
|
+
// Map iterates in insertion order — evict the oldest entry.
|
|
34
|
+
const oldest = seenDeliveries.keys().next().value;
|
|
35
|
+
if (oldest !== undefined) seenDeliveries.delete(oldest);
|
|
36
|
+
}
|
|
37
|
+
seenDeliveries.set(deliveryId, now + REPLAY_WINDOW_MS);
|
|
38
|
+
return false;
|
|
10
39
|
}
|
|
11
40
|
|
|
12
41
|
/**
|
|
@@ -41,6 +70,13 @@ interface WebhookPayload {
|
|
|
41
70
|
* Auth: HMAC-SHA256 only. Bearer dropped — single auth model means one
|
|
42
71
|
* env var, one shim, fewer customer mis-config paths (the disambiguation
|
|
43
72
|
* problem the v0.10 dual-handler shipped with).
|
|
73
|
+
*
|
|
74
|
+
* Replay protection (v0.21.1+): a signed delivery is only accepted while
|
|
75
|
+
* `deliveredAt` sits inside a ±5min window (401 `stale_delivery`
|
|
76
|
+
* otherwise — missing field counts as stale), and a `deliveryId` seen
|
|
77
|
+
* before within that window is rejected (401 `duplicate_delivery`).
|
|
78
|
+
* Payloads from a SaaS predating `deliveryId` skip dedup — the window
|
|
79
|
+
* remains the primary defense.
|
|
44
80
|
*/
|
|
45
81
|
export async function POST(request: Request): Promise<Response> {
|
|
46
82
|
const secret = process.env.SMKING_WEBHOOK_SECRET;
|
|
@@ -66,6 +102,23 @@ export async function POST(request: Request): Promise<Response> {
|
|
|
66
102
|
return Response.json({ error: "invalid_payload" }, { status: 400 });
|
|
67
103
|
}
|
|
68
104
|
|
|
105
|
+
// Replay protection — deliveredAt must sit inside a ±5min window
|
|
106
|
+
// (missing/unparseable counts as stale; SaaS has sent it since v0.11),
|
|
107
|
+
// then deliveryId dedup rejects re-sends of a delivery we already saw.
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const deliveredAtMs = payload.deliveredAt
|
|
110
|
+
? Date.parse(payload.deliveredAt)
|
|
111
|
+
: Number.NaN;
|
|
112
|
+
if (
|
|
113
|
+
Number.isNaN(deliveredAtMs) ||
|
|
114
|
+
Math.abs(now - deliveredAtMs) > REPLAY_WINDOW_MS
|
|
115
|
+
) {
|
|
116
|
+
return Response.json({ error: "stale_delivery" }, { status: 401 });
|
|
117
|
+
}
|
|
118
|
+
if (isDuplicateDelivery(payload.deliveryId, now)) {
|
|
119
|
+
return Response.json({ error: "duplicate_delivery" }, { status: 401 });
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
const kind = payload.kind;
|
|
70
123
|
if (typeof kind !== "string" || kind.length === 0) {
|
|
71
124
|
// Forward-compat: SaaS may emit kinds we haven't taught the SDK
|
package/src/types.ts
CHANGED
|
@@ -64,58 +64,34 @@ export type CmsContentType = "article" | "landing" | "listing";
|
|
|
64
64
|
// `<article class="prose">` around `<SmkingCms>` themselves.
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* v2 substrate block primitives.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* v2 substrate block primitives. Single source of truth lives in the
|
|
68
|
+
* monorepo's `packages/shared/src/types/cms-blocks.ts`; this package ships
|
|
69
|
+
* a vendored copy (`./cms-blocks`, regenerated via `pnpm sync:block-types`)
|
|
70
|
+
* because raw `src/` is published to npm and can't depend on the private
|
|
71
|
+
* workspace package. A vitest guard fails when the copy drifts.
|
|
72
72
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
export interface NavSnapshotEntry {
|
|
96
|
-
slug: string;
|
|
97
|
-
title: string | null;
|
|
98
|
-
excerpt: string | null;
|
|
99
|
-
featuredImageUrl: string | null;
|
|
100
|
-
/** ISO string. */
|
|
101
|
-
publishedAt: string | null;
|
|
102
|
-
contentType: "article" | "landing" | "listing" | null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface NavTaxonomyListProps extends ModuleHeader {
|
|
106
|
-
/** category-by-path: slug prefix; tag: taxonomies.slug */
|
|
107
|
-
source: "category-by-path" | "tag";
|
|
108
|
-
path: string;
|
|
109
|
-
limit: number;
|
|
110
|
-
layout: NavLayout;
|
|
111
|
-
/** Publish-time materialized — never fetched live. */
|
|
112
|
-
snapshot?: NavSnapshotEntry[];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type Block =
|
|
116
|
-
| { component: "hero"; id: string; props: HeroProps }
|
|
117
|
-
| { component: "article"; id: string; props: ArticleProps }
|
|
118
|
-
| { component: "nav-taxonomy-list"; id: string; props: NavTaxonomyListProps };
|
|
73
|
+
import type { Block } from "./cms-blocks";
|
|
74
|
+
|
|
75
|
+
export type {
|
|
76
|
+
ArticleProps,
|
|
77
|
+
Block,
|
|
78
|
+
BlockComponent,
|
|
79
|
+
CategoryCardSnapshot,
|
|
80
|
+
CategoryIndexItem,
|
|
81
|
+
CategoryIndexProps,
|
|
82
|
+
HeroProps,
|
|
83
|
+
ImageProps,
|
|
84
|
+
ModuleHeader,
|
|
85
|
+
NavLayout,
|
|
86
|
+
NavSnapshotEntry,
|
|
87
|
+
NavTaxonomyListProps,
|
|
88
|
+
RecentPostsProps,
|
|
89
|
+
RelatedPostsProps,
|
|
90
|
+
SearchProps,
|
|
91
|
+
SlideshowProps,
|
|
92
|
+
SlideshowSlideProps,
|
|
93
|
+
SocialShareProps,
|
|
94
|
+
} from "./cms-blocks";
|
|
119
95
|
|
|
120
96
|
/**
|
|
121
97
|
* A single published CMS page returned by the smking public API
|