@agntcms/next 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { S as SectionSchema, x as FieldValueFor, w as PageSummary, F as FormLookup, o as SubmissionStorageAdapter, P as Page } from './form-BqY0H1V5.js';
2
+ import { C as ContentStorageAdapter } from './assets-P8OCigDG.js';
3
+ import { G as Global } from './global-CV23g5Bn.js';
4
+
5
+ /**
6
+ * Mode discriminator for `getContent`. `'published'` is the prod hot
7
+ * path (bare data). `'preview'` is the editor path (data wrapped with
8
+ * origin metadata).
9
+ */
10
+ type PreviewMode = 'preview' | 'published';
11
+ /**
12
+ * Origin metadata carried by every `PreviewField<T>` in preview mode.
13
+ *
14
+ * `pageSlug`, `sectionId`, and `fieldPath` together address the field in
15
+ * the content store uniquely enough for the save round-trip to write
16
+ * back to the correct draft file and the correct field inside its
17
+ * section payload.
18
+ *
19
+ * `source` tells the UI and save flow which bucket actually served the
20
+ * data: `'draft'` when a draft existed and was read, `'published'` when
21
+ * preview mode fell back to the published snapshot because no draft
22
+ * existed yet. This affects the first save: saving against a
23
+ * `'published'` source creates a new draft; saving against a `'draft'`
24
+ * source overwrites the existing one.
25
+ *
26
+ * `revision` is a content-addressable hash (SHA-256 hex) of the
27
+ * serialized page bytes from the source bucket at read time. See the
28
+ * file header for the rationale; the hash discriminates ACTUAL content
29
+ * drift without needing adapter-level bookkeeping.
30
+ *
31
+ * `kind` discriminates between a field that lives in a page section
32
+ * (`'page'`, the default) and a field that lives in a standalone global
33
+ * read via `getGlobal` (`'global'`). When `kind` is `'global'`,
34
+ * `globalName` carries the global's name so the save round-trip can
35
+ * route to `/api/agntcms/global/save` instead of the page draft
36
+ * endpoint. The discriminator is OPTIONAL for backward compatibility:
37
+ * existing call sites that don't set it are treated as page origins.
38
+ *
39
+ * Why not split into two unrelated origin types? Every editable widget
40
+ * already reads `origin.fieldPath` and (for the agent ✨ button)
41
+ * `origin.pageSlug`/`origin.sectionId`. Keeping a single shape with all
42
+ * fields present (populated with sentinel values on the global path,
43
+ * matching the convention `wrapGlobalData` introduced in AdminModal)
44
+ * avoids touching every editable component. Consumers that care about
45
+ * the routing decision branch on `origin.kind`.
46
+ */
47
+ interface PreviewFieldOrigin {
48
+ readonly pageSlug: string;
49
+ readonly sectionId: string;
50
+ readonly fieldPath: string;
51
+ readonly source: 'draft' | 'published';
52
+ readonly revision: string;
53
+ /**
54
+ * Discriminator: `'page'` (default, when omitted) means the field
55
+ * lives inside a page draft; `'global'` means it lives inside a
56
+ * standalone global. New in v0.2 (Phase 2 globals work).
57
+ */
58
+ readonly kind?: 'page' | 'global';
59
+ /**
60
+ * The global's name. Present iff `kind === 'global'`. Consumers that
61
+ * route saves on the global endpoint read this value.
62
+ */
63
+ readonly globalName?: string;
64
+ }
65
+ /**
66
+ * A field value in preview mode: the bare value plus its origin metadata
67
+ * and a brand so downstream code can distinguish a wrapped preview field
68
+ * from a bare published value both at compile time and at runtime.
69
+ *
70
+ * The brand uses a regular property `__agntcmsPreview: true` rather
71
+ * than a `unique symbol`. The original T-007 design used a `declare`-only
72
+ * unique symbol for the brand, which is structurally unforgeable at
73
+ * compile time. However, `declare const` symbols exist only in the type
74
+ * system — they have no runtime identity, so the implementation (T-008)
75
+ * cannot actually SET a symbol-keyed property on the wrapper objects it
76
+ * constructs. Using a string-keyed property solves this: the runtime can
77
+ * set it, the `EditableText`/`EditableImage` components (T-016) can
78
+ * detect it with a cheap `'__agntcmsPreview' in field` check, and it is
79
+ * still unforgeable enough for v1 (no user type would accidentally have
80
+ * a `__agntcmsPreview` property). The double-underscore prefix signals
81
+ * "framework internal — do not depend on this key".
82
+ *
83
+ * The `value` key carries the underlying data (e.g. a `string` for
84
+ * `TextField` or `ImageField`). Client-side code unwraps by reading
85
+ * `field.value`; servers read `field.origin` to compute where a save
86
+ * must land.
87
+ */
88
+ interface PreviewField<T> {
89
+ readonly __agntcmsPreview: true;
90
+ readonly value: T;
91
+ readonly origin: PreviewFieldOrigin;
92
+ }
93
+ /**
94
+ * `FieldIn<Mode, T>` is the mode-aware wrapper type. It is the SINGLE
95
+ * axiom on which everything else in this file depends:
96
+ *
97
+ * - `FieldIn<'preview', T>` → `PreviewField<T>`
98
+ * - `FieldIn<'published', T>` → `T` (structurally identical, zero cost)
99
+ *
100
+ * Written as a distributive conditional over a naked `Mode` parameter so
101
+ * that when `Mode` is itself a union, `FieldIn<Mode, T>` distributes over
102
+ * it. This is what makes the single `getContent` call site correctly
103
+ * return `PreviewField<T> | T` (rather than a widened `never`) when the
104
+ * caller's `mode` is known only as `'preview' | 'published'`.
105
+ */
106
+ type FieldIn<Mode extends PreviewMode, T> = Mode extends 'preview' ? PreviewField<T> : Mode extends 'published' ? T : never;
107
+ /**
108
+ * Projects a section schema `S` into its mode-resolved field shape.
109
+ * Each key in `S` is mapped through `FieldValueFor<S[K]>` to its runtime
110
+ * value type, and then wrapped via `FieldIn<Mode, _>`.
111
+ *
112
+ * The double conditional keeps distribution working: `Mode` must remain
113
+ * naked in the outermost position so a union `Mode` distributes here.
114
+ * `S[K]` is passed through `FieldValueFor` separately (non-distributive)
115
+ * because each key resolves independently.
116
+ *
117
+ * Mapped type keys are preserved as-is (no `-readonly`, no `-?`) so the
118
+ * readonly-ness and optionality of the schema's own keys are not
119
+ * dropped.
120
+ */
121
+ type PageContent<Mode extends PreviewMode, S extends SectionSchema> = Mode extends 'preview' ? {
122
+ readonly [K in keyof S]: PreviewField<FieldValueFor<S[K]>>;
123
+ } : Mode extends 'published' ? {
124
+ readonly [K in keyof S]: FieldValueFor<S[K]>;
125
+ } : never;
126
+ /**
127
+ * Parameters for a single `getContent` call.
128
+ *
129
+ * `slug` identifies the page. `mode` is the dual-mode discriminator;
130
+ * keeping it as a generic `Mode extends PreviewMode` is what lets the
131
+ * return type narrow in lockstep with the caller's `mode` value.
132
+ *
133
+ * Additional fields (e.g. locale, draft id, preview token) are
134
+ * deliberately out of scope for T-007. They can be added additively
135
+ * without breaking the mode-resolution mechanism locked in here.
136
+ */
137
+ interface GetContentOptions<Mode extends PreviewMode> {
138
+ readonly slug: string;
139
+ readonly mode: Mode;
140
+ }
141
+ /**
142
+ * The public `getContent` function type. Single call site, generic over
143
+ * `Mode` and over the section schema `S`, returning `null` when the
144
+ * requested page does not exist in the mode's resolved storage.
145
+ *
146
+ * T-008 ships the runtime implementation that satisfies this type.
147
+ * Typed as a function type (not an interface with a call signature) so
148
+ * callers using `typeof getContent` get a clean arrow-function shape in
149
+ * tooltips.
150
+ */
151
+ type GetContent = <Mode extends PreviewMode, S extends SectionSchema>(options: GetContentOptions<Mode>) => Promise<PageContent<Mode, S> | null>;
152
+
153
+ interface GetGlobalInput {
154
+ readonly name: string;
155
+ readonly mode: PreviewMode;
156
+ }
157
+ /**
158
+ * The runtime function shape for `getGlobal`. Returned as part of the
159
+ * `Runtime` surface from `createRuntime`. Returns `null` when the named
160
+ * global doesn't exist.
161
+ */
162
+ type GetGlobal = (options: GetGlobalInput) => Promise<Global | null>;
163
+
164
+ /** Sort direction for `listPages`. Defaults to `'newest'`. */
165
+ type ListPagesSort = 'newest' | 'oldest';
166
+ interface ListPagesInput {
167
+ /** Case-sensitive exact-match filter on `Page.tags`. */
168
+ readonly tag?: string;
169
+ /** Maximum number of results. No upper bound; passing `0` returns []. */
170
+ readonly limit?: number;
171
+ /** Sort direction. Defaults to `'newest'`. */
172
+ readonly sort?: ListPagesSort;
173
+ }
174
+ /**
175
+ * Read metadata-only summaries of every PUBLISHED page, optionally
176
+ * filtered by tag and capped by limit.
177
+ *
178
+ * V1 limitation: returns published pages only regardless of preview /
179
+ * published context. Drafts are not surfaced in lists. To see a draft
180
+ * in a `PostList`, the page must first be published. See file header
181
+ * and ARCHITECTURE.md §12 for the rationale and roadmap.
182
+ */
183
+ type ListPages = (input?: ListPagesInput) => Promise<ReadonlyArray<PageSummary>>;
184
+ interface CreateListPagesDeps {
185
+ readonly contentAdapter: ContentStorageAdapter;
186
+ }
187
+ /**
188
+ * Build the `listPages` runtime function.
189
+ *
190
+ * V1 limitation: the returned function reads PUBLISHED pages only. A
191
+ * draft-only page (never published) will not appear in the result, even
192
+ * when the surrounding request is in preview mode. See file header and
193
+ * ARCHITECTURE.md §12.
194
+ */
195
+ declare const createListPages: ({ contentAdapter, }: CreateListPagesDeps) => ListPages;
196
+
197
+ /** Result of a `submitForm` call. */
198
+ type SubmitFormResult = {
199
+ readonly ok: true;
200
+ readonly stored: true;
201
+ readonly id: string;
202
+ } | {
203
+ readonly ok: true;
204
+ readonly stored: false;
205
+ readonly suppressed: 'honeypot';
206
+ } | {
207
+ readonly ok: false;
208
+ readonly error: 'unknown_form';
209
+ } | {
210
+ readonly ok: false;
211
+ readonly error: 'validation_failed';
212
+ readonly errors: Record<string, string>;
213
+ };
214
+ /** Dependencies for `submitForm`. */
215
+ interface SubmitFormDeps {
216
+ /**
217
+ * A `FormLookup`-shaped object: anything with `get(name)` returning a
218
+ * form definition. The concrete `FormRegistry` from `forms/registry.ts`
219
+ * structurally satisfies this interface, so the runtime can stay
220
+ * dependency-free of `forms/` (per ARCHITECTURE.md §8 dep graph).
221
+ */
222
+ readonly forms: FormLookup;
223
+ readonly submissionAdapter: SubmissionStorageAdapter;
224
+ /** Optional ID generator for tests; defaults to a ULID-like generator. */
225
+ readonly generateId?: () => string;
226
+ /** Optional clock for tests; defaults to `() => new Date().toISOString()`. */
227
+ readonly now?: () => string;
228
+ }
229
+ /** Input to `submitForm`. */
230
+ interface SubmitFormInput {
231
+ readonly formName: string;
232
+ /** The raw payload from the request. Validated here, NOT in the handler. */
233
+ readonly payload: Readonly<Record<string, unknown>>;
234
+ }
235
+ /**
236
+ * Build the runtime function that handles form submissions.
237
+ *
238
+ * Returns a `submitForm(input)` that:
239
+ * 1. Looks up the form definition.
240
+ * 2. Honors honeypot.
241
+ * 3. Validates payload.
242
+ * 4. Stamps id+submittedAt and stores via the adapter.
243
+ */
244
+ declare function createSubmitForm(deps: SubmitFormDeps): (input: SubmitFormInput) => Promise<SubmitFormResult>;
245
+ /** Type of the function returned by `createSubmitForm`. */
246
+ type SubmitForm = ReturnType<typeof createSubmitForm>;
247
+
248
+ /** Dependencies for the runtime. */
249
+ interface RuntimeOptions {
250
+ readonly contentAdapter: ContentStorageAdapter;
251
+ /**
252
+ * Form registry — required for `submitForm` to look up form definitions.
253
+ * Accepts any `FormLookup`-shaped object (the concrete `FormRegistry`
254
+ * from `forms/registry.ts` satisfies this structurally). Optional for
255
+ * backward compatibility: when omitted, `submitForm` is wired to an
256
+ * empty lookup, so any submission attempt returns `unknown_form`.
257
+ * Templates that use forms (ARCHITECTURE.md §6.5) MUST pass a registry
258
+ * built from `defineConfig({ forms: [...] })`.
259
+ */
260
+ readonly forms?: FormLookup;
261
+ /**
262
+ * Submission storage adapter. Optional for backward compatibility: when
263
+ * omitted, `submitForm` is wired to a no-op adapter that throws on store.
264
+ * Templates that use forms MUST pass an adapter (FS or webhook).
265
+ */
266
+ readonly submissionAdapter?: SubmissionStorageAdapter;
267
+ }
268
+ /**
269
+ * The runtime surface returned by `createRuntime`. This is what
270
+ * `defineConfig` (T-019) will later wire up and what the handlers
271
+ * layer consumes.
272
+ */
273
+ interface Runtime {
274
+ /**
275
+ * Read a page in the requested mode.
276
+ *
277
+ * In 'published' mode the bare `Page` from the adapter is returned
278
+ * as-is — zero allocation, no wrapping.
279
+ *
280
+ * In 'preview' mode every field value in every section is wrapped in
281
+ * a `PreviewField` carrying origin metadata. The runtime tries the
282
+ * draft bucket first and falls back to the published bucket.
283
+ *
284
+ * Returns `null` when neither bucket has a page for the given slug.
285
+ */
286
+ readonly getContent: (options: GetContentInput) => Promise<Page | null>;
287
+ /**
288
+ * Thin wrapper over the adapter's `publishDraft`. Git commits are
289
+ * T-009's scope — this function does NOT touch git.
290
+ */
291
+ readonly publishDraft: (slug: string) => Promise<Page>;
292
+ /**
293
+ * Read a named global in the requested mode.
294
+ *
295
+ * Mirrors `getContent`'s dual nature (ARCHITECTURE.md §6) for
296
+ * standalone globals. In `'published'` mode returns the bare
297
+ * `Global`; in `'preview'` mode wraps every field in `PreviewField`
298
+ * with a `kind: 'global'` origin so editable widgets route saves to
299
+ * `/api/agntcms/global/save`.
300
+ *
301
+ * Returns `null` when no global with `name` exists. Globals have no
302
+ * draft/publish cycle (see domain/global.ts) so there is no
303
+ * fall-back branch.
304
+ */
305
+ readonly getGlobal: GetGlobal;
306
+ /**
307
+ * Accept a form submission. See `runtime/submitForm.ts` for the full
308
+ * pipeline (form lookup, honeypot, validation, store). Rate limiting
309
+ * is handled at the handler layer, not here.
310
+ *
311
+ * When the runtime was built without a `forms` registry or a
312
+ * `submissionAdapter`, `submitForm` returns `{ ok: false, error: 'unknown_form' }`
313
+ * for every call so downstream code can fail closed without crashing.
314
+ */
315
+ readonly submitForm: SubmitForm;
316
+ /**
317
+ * Return metadata-only summaries of every published page, optionally
318
+ * filtered by a single tag and capped by `limit`. Used by user-defined
319
+ * listing sections (e.g. PostList) for blog-index-style queries.
320
+ *
321
+ * See `runtime/listPages.ts` for the sort and filter semantics
322
+ * (ARCHITECTURE.md §4).
323
+ */
324
+ readonly listPages: ListPages;
325
+ }
326
+
327
+ interface GetContentInput {
328
+ readonly slug: string;
329
+ readonly mode: PreviewMode;
330
+ }
331
+ /**
332
+ * Create the runtime that powers `getContent` and `publishDraft`.
333
+ *
334
+ * The adapter is injected — no global state, no singleton. This is the
335
+ * shape `defineConfig` (T-019) will later construct under the hood.
336
+ */
337
+ declare function createRuntime(options: RuntimeOptions): Runtime;
338
+
339
+ /** Result of a rate-limit check. */
340
+ interface RateLimitResult {
341
+ /** When `false`, the caller MUST reject the request with HTTP 429. */
342
+ readonly allowed: boolean;
343
+ /** Number of requests counted in the current window after this call. */
344
+ readonly count: number;
345
+ /** Epoch milliseconds when the current window resets. */
346
+ readonly resetAt: number;
347
+ }
348
+ /** Options for `createRateLimit`. */
349
+ interface RateLimitOptions {
350
+ /** Max requests per window per (ip, formName) key. Default: 5. */
351
+ readonly perWindow: number;
352
+ /** Window length in milliseconds. Default: 60_000 (1 minute). */
353
+ readonly windowMs: number;
354
+ /**
355
+ * Clock injection point for tests. Called once per check to read the
356
+ * current time. Defaults to `Date.now`.
357
+ */
358
+ readonly now?: () => number;
359
+ /**
360
+ * Maximum number of (ip, formName) buckets retained in memory. When
361
+ * the map reaches this size, `check()` lazily sweeps expired buckets;
362
+ * if still at the cap, the oldest-resetAt buckets are dropped to make
363
+ * room. Default: 10_000. Must be a positive integer.
364
+ *
365
+ * Why: a public submit endpoint with one-off IPs (CDN edges, mobile
366
+ * NAT pools, attack traffic) would otherwise grow the map without
367
+ * bound until process restart.
368
+ */
369
+ readonly maxBuckets?: number;
370
+ }
371
+ interface RateLimit {
372
+ /**
373
+ * Record one request and return whether it is allowed under the current
374
+ * window. Always counts (even rejected calls) — this matches the typical
375
+ * abuse-mitigation expectation: a flood that hits the limit shouldn't
376
+ * "free" itself by spinning down the counter.
377
+ */
378
+ check(ip: string, formName: string): RateLimitResult;
379
+ /**
380
+ * Test-only utility: clear all buckets. Useful between tests to keep
381
+ * them order-independent without recreating the limiter.
382
+ */
383
+ reset(): void;
384
+ }
385
+ /**
386
+ * Build an in-memory rate limiter. Each created limiter is independent
387
+ * (no module-level singleton).
388
+ */
389
+ declare function createRateLimit(options: RateLimitOptions): RateLimit;
390
+
391
+ export { type FieldIn as F, type GetGlobal as G, type ListPages as L, type PreviewMode as P, type RateLimit as R, type SubmitForm as S, type GetContent as a, type GetContentInput as b, type GetContentOptions as c, type GetGlobalInput as d, type ListPagesInput as e, type ListPagesSort as f, type PageContent as g, type PreviewField as h, type PreviewFieldOrigin as i, type RateLimitOptions as j, type RateLimitResult as k, type Runtime as l, type RuntimeOptions as m, type SubmitFormDeps as n, type SubmitFormInput as o, type SubmitFormResult as p, createListPages as q, createRateLimit as r, createRuntime as s, createSubmitForm as t };