agent-cms 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,183 @@
1
- import { $ as listRecords, A as scheduleUnpublish, B as getAsset, C as SearchInput, D as clearSchedule, E as UpdateModelInput, G as updateAssetMetadata, H as listAssets, J as publishRecord, M as deleteLocale, N as listLocales, O as runScheduledTransitions, Q as getRecord, R as createAsset, S as SearchAssetsInput, T as UpdateFieldInput, U as replaceAsset, W as searchAssets, X as bulkCreateRecords, Y as unpublishRecord, Z as createRecord, _ as PatchBlocksInput, _t as updateModel, a as exportSchema, at as getVersion, b as ReorderInput, bt as VectorizeContext, c as CreateAssetInput, ct as HooksContext, d as CreateLocaleInput, dt as listFields, et as patchBlocksForField, f as CreateModelInput, ft as updateField, g as ImportSchemaInput, gt as listModels, ht as getModel, i as validateEditorToken, j as createLocale, k as schedulePublish, l as CreateEditorTokenInput, lt as createField, m as CreateUploadUrlInput, mt as deleteModel, n as listEditorTokens, nt as removeRecord, o as importSchema, ot as listVersions, p as CreateRecordInput, pt as createModel, r as revokeEditorToken, rt as reorderRecords, s as BulkCreateRecordsInput, st as restoreVersion, t as createEditorToken, tt as patchRecord, u as CreateFieldInput, ut as deleteField, v as PatchRecordInput, vt as reindexAll, w as UpdateAssetMetadataInput, x as ScheduleRecordInput, y as ReindexSearchInput, yt as search, z as deleteAsset } from "./token-service-BDjccMmz.mjs";
2
- import { J as ValidationError, X as isCmsError, Y as errorToResponse, q as UnauthorizedError } from "./structured-text-service-B4xSlUg_.mjs";
1
+ import { $ as bulkCreateRecords, A as clearSchedule, C as ReorderInput, Ct as search, D as UpdateAssetMetadataInput, E as SearchInput, F as deleteLocale, G as listAssets, H as deleteAsset, I as listLocales, J as updateAssetMetadata, K as replaceAsset, M as schedulePublish, N as scheduleUnpublish, O as UpdateFieldInput, P as createLocale, Q as unpublishRecord, S as ReindexSearchInput, St as reindexAll, T as SearchAssetsInput, U as getAsset, V as createAsset, Z as publishRecord, _ as CreateUploadUrlInput, _t as deleteModel, a as listEditorTokens, at as removeRecord, b as PatchBlocksInput, bt as listModels, c as exportSchema, ct as getVersion, d as CreateAssetInput, dt as HooksContext, et as createRecord, f as CreateEditorTokenInput, ft as createField, g as CreateRecordInput, gt as createModel, h as CreateModelInput, ht as updateField, i as createEditorToken, it as patchRecord, j as runScheduledTransitions, k as UpdateModelInput, l as importSchema, lt as listVersions, m as CreateLocaleInput, mt as listFields, nt as listRecords, o as revokeEditorToken, ot as reorderRecords, p as CreateFieldInput, pt as deleteField, q as searchAssets, r as validatePreviewToken, rt as patchBlocksForField, s as validateEditorToken, t as createPreviewToken, tt as getRecord, u as BulkCreateRecordsInput, ut as restoreVersion, vt as getModel, w as ScheduleRecordInput, wt as VectorizeContext, x as PatchRecordInput, xt as updateModel, y as ImportSchemaInput } from "./preview-service-C9Tmhdye.mjs";
2
+ import { E as getLinkTargets, J as ValidationError, L as decodeJsonRecordStringOr, W as NotFoundError, X as isCmsError, Y as errorToResponse, q as UnauthorizedError } from "./structured-text-service-BJkqWRkq.mjs";
3
3
  import { D1Client } from "@effect/sql-d1";
4
4
  import { Cause, Effect, Layer, Logger, Option, ParseResult, Schema } from "effect";
5
- import { HttpApp, HttpRouter, HttpServerError, HttpServerRequest, HttpServerResponse } from "@effect/platform";
5
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApp, HttpRouter, HttpServerError, HttpServerRequest, HttpServerResponse, OpenApi } from "@effect/platform";
6
6
  import { SqlClient } from "@effect/sql";
7
+ //#region src/services/path-service.ts
8
+ /**
9
+ * Canonical path resolver — batch-resolves canonical_path_template for all
10
+ * published records of a model, traversing link fields via dot notation.
11
+ *
12
+ * Template examples:
13
+ * /blog/{slug} — flat field
14
+ * /blog/{category.slug}/{slug} — one-hop link traversal
15
+ * /{category.parent.slug}/{category.slug}/{slug} — multi-hop
16
+ *
17
+ * Unresolvable tokens (null link, missing field) are left as-is: {token}.
18
+ */
19
+ const MAX_DEPTH = 10;
20
+ function parseTemplateTokens(template) {
21
+ const tokens = [];
22
+ const re = /\{([^}]+)\}/g;
23
+ let match;
24
+ while ((match = re.exec(template)) !== null) tokens.push({
25
+ raw: match[1],
26
+ segments: match[1].split(".")
27
+ });
28
+ return tokens;
29
+ }
30
+ function parseValidators(raw) {
31
+ if (!raw || raw === "") return {};
32
+ return decodeJsonRecordStringOr(raw, {});
33
+ }
34
+ /**
35
+ * Resolve canonical paths for all published records of a model.
36
+ * Returns array of { id, path, lastmod }.
37
+ */
38
+ function resolveCanonicalPaths(modelApiKey) {
39
+ return Effect.gen(function* () {
40
+ const sql = yield* SqlClient.SqlClient;
41
+ const models = yield* sql.unsafe("SELECT * FROM models WHERE api_key = ?", [modelApiKey]);
42
+ if (models.length === 0) return yield* new NotFoundError({
43
+ entity: "Model",
44
+ id: modelApiKey
45
+ });
46
+ const model = models[0];
47
+ const template = model.canonical_path_template;
48
+ if (!template) return yield* new ValidationError({ message: `Model "${modelApiKey}" has no canonical_path_template. Set one via update_model before resolving paths.` });
49
+ const tokens = parseTemplateTokens(template);
50
+ if (tokens.length === 0) return yield* new ValidationError({ message: `Template "${template}" contains no {field} tokens.` });
51
+ const fields = yield* sql.unsafe("SELECT * FROM fields WHERE model_id = ? ORDER BY position", [model.id]);
52
+ const fieldsByApiKey = new Map(fields.map((f) => [f.api_key, f]));
53
+ const neededColumns = new Set([
54
+ "id",
55
+ "_published_at",
56
+ "_updated_at"
57
+ ]);
58
+ for (const token of tokens) neededColumns.add(token.segments[0]);
59
+ const columnList = [...neededColumns].map((c) => `"${c}"`).join(", ");
60
+ const records = yield* sql.unsafe(`SELECT ${columnList} FROM "content_${modelApiKey}" WHERE "_status" IN ('published', 'updated')`);
61
+ if (records.length === 0) return [];
62
+ const linkTokens = tokens.filter((t) => t.segments.length > 1);
63
+ const resolvedValues = /* @__PURE__ */ new Map();
64
+ for (const rec of records) resolvedValues.set(rec.id, /* @__PURE__ */ new Map());
65
+ for (const token of tokens) if (token.segments.length === 1) {
66
+ const fieldName = token.segments[0];
67
+ for (const rec of records) {
68
+ const value = rec[fieldName];
69
+ if (value !== null && value !== void 0) resolvedValues.get(rec.id).set(token.raw, String(value));
70
+ }
71
+ }
72
+ if (linkTokens.length > 0) yield* resolveLinkedTokens(sql, linkTokens, records, fieldsByApiKey, resolvedValues);
73
+ const results = [];
74
+ for (const rec of records) {
75
+ const recId = rec.id;
76
+ const values = resolvedValues.get(recId);
77
+ const path = template.replace(/\{([^}]+)\}/g, (_match, tokenRaw) => {
78
+ const resolved = values.get(tokenRaw);
79
+ if (resolved !== void 0) return encodeURIComponent(resolved);
80
+ return `{${tokenRaw}}`;
81
+ });
82
+ const publishedAt = rec._published_at;
83
+ const updatedAt = rec._updated_at;
84
+ const lastmod = laterTimestamp(publishedAt, updatedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
85
+ results.push({
86
+ id: recId,
87
+ path,
88
+ lastmod
89
+ });
90
+ }
91
+ return results;
92
+ });
93
+ }
94
+ function laterTimestamp(a, b) {
95
+ if (!a && !b) return null;
96
+ if (!a) return b;
97
+ if (!b) return a;
98
+ return a > b ? a : b;
99
+ }
100
+ /**
101
+ * Resolve multi-segment tokens by traversing link fields breadth-first.
102
+ *
103
+ * For each hop level, we:
104
+ * 1. Collect all link IDs we need to fetch
105
+ * 2. Look up the target model from field validators
106
+ * 3. Batch-fetch all linked records
107
+ * 4. Continue to next hop with the linked records
108
+ */
109
+ function resolveLinkedTokens(sql, tokens, records, fieldsByApiKey, resolvedValues) {
110
+ return Effect.gen(function* () {
111
+ const tokenFrontiers = /* @__PURE__ */ new Map();
112
+ for (const token of tokens) {
113
+ const frontierMap = /* @__PURE__ */ new Map();
114
+ for (const rec of records) {
115
+ const recId = rec.id;
116
+ const frontier = /* @__PURE__ */ new Map();
117
+ frontier.set(recId, rec);
118
+ frontierMap.set(recId, frontier);
119
+ }
120
+ tokenFrontiers.set(token.raw, frontierMap);
121
+ }
122
+ for (const token of tokens) {
123
+ const segments = token.segments;
124
+ let currentRecords = /* @__PURE__ */ new Map();
125
+ for (const rec of records) currentRecords.set(rec.id, rec);
126
+ let currentFieldsByApiKey = fieldsByApiKey;
127
+ let hopsCompleted = 0;
128
+ const hopsNeeded = segments.length - 1;
129
+ for (let hop = 0; hop < hopsNeeded; hop++) {
130
+ if (hop >= MAX_DEPTH) break;
131
+ const fieldName = segments[hop];
132
+ const field = currentFieldsByApiKey.get(fieldName);
133
+ if (!field || field.field_type !== "link") break;
134
+ const targetApiKeys = getLinkTargets(parseValidators(field.validators));
135
+ if (!targetApiKeys || targetApiKeys.length === 0) break;
136
+ const linkIdToOriginalRecords = /* @__PURE__ */ new Map();
137
+ for (const [origId, rec] of currentRecords) {
138
+ const linkId = rec[fieldName];
139
+ if (typeof linkId === "string" && linkId) {
140
+ const list = linkIdToOriginalRecords.get(linkId) ?? [];
141
+ list.push(origId);
142
+ linkIdToOriginalRecords.set(linkId, list);
143
+ }
144
+ }
145
+ const linkedRecords = /* @__PURE__ */ new Map();
146
+ if (linkIdToOriginalRecords.size > 0) {
147
+ const idsToFetch = [...linkIdToOriginalRecords.keys()];
148
+ for (const targetApiKey of targetApiKeys) {
149
+ if (idsToFetch.length === 0) break;
150
+ const placeholders = idsToFetch.map(() => "?").join(", ");
151
+ const rows = yield* sql.unsafe(`SELECT * FROM "content_${targetApiKey}" WHERE "id" IN (${placeholders})`, idsToFetch);
152
+ for (const row of rows) linkedRecords.set(row.id, row);
153
+ }
154
+ }
155
+ const nextRecords = /* @__PURE__ */ new Map();
156
+ for (const [origId, rec] of currentRecords) {
157
+ const linkId = rec[fieldName];
158
+ if (typeof linkId === "string" && linkId) {
159
+ const linked = linkedRecords.get(linkId);
160
+ if (linked) nextRecords.set(origId, linked);
161
+ }
162
+ }
163
+ currentRecords = nextRecords;
164
+ const targetModel = yield* sql.unsafe(`SELECT * FROM "models" WHERE "api_key" = ?`, [targetApiKeys[0]]);
165
+ if (targetModel.length === 0) break;
166
+ const targetFields = yield* sql.unsafe(`SELECT * FROM "fields" WHERE "model_id" = ? ORDER BY position`, [targetModel[0].id]);
167
+ currentFieldsByApiKey = new Map(targetFields.map((f) => [f.api_key, f]));
168
+ hopsCompleted++;
169
+ }
170
+ if (hopsCompleted === hopsNeeded) {
171
+ const leafField = segments[segments.length - 1];
172
+ for (const [origId, linkedRec] of currentRecords) {
173
+ const value = linkedRec[leafField];
174
+ if (value !== null && value !== void 0) resolvedValues.get(origId).set(token.raw, String(value));
175
+ }
176
+ }
177
+ }
178
+ });
179
+ }
180
+ //#endregion
7
181
  //#region src/migrations.ts
8
182
  /**
9
183
  * Embedded schema migrations — runs automatically on first request.
@@ -15,6 +189,8 @@ const MIGRATIONS = [{
15
189
  `CREATE TABLE IF NOT EXISTS "assets" (
16
190
  "id" text PRIMARY KEY,
17
191
  "filename" text NOT NULL,
192
+ "basename" text,
193
+ "format" text,
18
194
  "mime_type" text NOT NULL,
19
195
  "size" integer NOT NULL,
20
196
  "width" integer,
@@ -43,6 +219,7 @@ const MIGRATIONS = [{
43
219
  "has_draft" integer DEFAULT true NOT NULL,
44
220
  "all_locales_required" integer DEFAULT 0 NOT NULL,
45
221
  "ordering" text,
222
+ "canonical_path_template" text,
46
223
  "created_at" text NOT NULL,
47
224
  "updated_at" text NOT NULL
48
225
  )`,
@@ -117,24 +294,14 @@ const MIGRATIONS = [{
117
294
  "last_used_at" TEXT,
118
295
  "expires_at" TEXT
119
296
  )`,
120
- `CREATE UNIQUE INDEX IF NOT EXISTS "idx_editor_tokens_secret_hash" ON "editor_tokens" ("secret_hash")`
121
- ]
122
- }, {
123
- version: 2,
124
- statements: [
125
- `ALTER TABLE "assets" ADD COLUMN "basename" text`,
126
- `ALTER TABLE "assets" ADD COLUMN "format" text`,
127
- `UPDATE "assets"
128
- SET "basename" = CASE
129
- WHEN instr("filename", '.') > 0 THEN substr("filename", 1, length("filename") - length(substr("filename", instr("filename", '.') + 1)) - 1)
130
- ELSE "filename"
131
- END`,
132
- `UPDATE "assets"
133
- SET "format" = lower(CASE
134
- WHEN instr("filename", '.') > 0 THEN substr("filename", instr("filename", '.') + 1)
135
- WHEN instr("mime_type", '/') > 0 THEN substr("mime_type", instr("mime_type", '/') + 1)
136
- ELSE 'bin'
137
- END)`,
297
+ `CREATE UNIQUE INDEX IF NOT EXISTS "idx_editor_tokens_secret_hash" ON "editor_tokens" ("secret_hash")`,
298
+ `CREATE TABLE IF NOT EXISTS "preview_tokens" (
299
+ "id" text PRIMARY KEY,
300
+ "token_hash" text NOT NULL UNIQUE,
301
+ "expires_at" text NOT NULL,
302
+ "created_at" text NOT NULL DEFAULT (datetime('now'))
303
+ )`,
304
+ `CREATE INDEX IF NOT EXISTS "idx_preview_tokens_hash" ON "preview_tokens" ("token_hash")`,
138
305
  `CREATE INDEX IF NOT EXISTS "idx_assets_basename" ON "assets" ("basename")`,
139
306
  `CREATE INDEX IF NOT EXISTS "idx_assets_format" ON "assets" ("format")`
140
307
  ]
@@ -182,6 +349,159 @@ function actorFromHeaders(headers) {
182
349
  };
183
350
  }
184
351
  //#endregion
352
+ //#region src/http/api/models.ts
353
+ /**
354
+ * HttpApiGroup for content model endpoints.
355
+ *
356
+ * Defines the declarative API shape — handlers are implemented separately
357
+ * via HttpApiBuilder.group().
358
+ */
359
+ const ModelResponse = Schema.Struct({
360
+ id: Schema.String,
361
+ name: Schema.String,
362
+ apiKey: Schema.String,
363
+ isBlock: Schema.Boolean,
364
+ singleton: Schema.Boolean,
365
+ sortable: Schema.Boolean,
366
+ tree: Schema.Boolean,
367
+ hasDraft: Schema.Boolean,
368
+ allLocalesRequired: Schema.Boolean,
369
+ ordering: Schema.NullOr(Schema.String),
370
+ canonicalPathTemplate: Schema.NullOr(Schema.String),
371
+ createdAt: Schema.String,
372
+ updatedAt: Schema.String
373
+ });
374
+ const ModelWithFieldsResponse = Schema.Struct({
375
+ id: Schema.String,
376
+ name: Schema.String,
377
+ api_key: Schema.String,
378
+ is_block: Schema.Number,
379
+ singleton: Schema.Number,
380
+ sortable: Schema.Number,
381
+ tree: Schema.Number,
382
+ has_draft: Schema.Number,
383
+ all_locales_required: Schema.Number,
384
+ ordering: Schema.NullOr(Schema.String),
385
+ canonical_path_template: Schema.NullOr(Schema.String),
386
+ created_at: Schema.String,
387
+ updated_at: Schema.String,
388
+ fields: Schema.Array(Schema.Unknown)
389
+ });
390
+ const DeleteModelResponse = Schema.Struct({
391
+ deleted: Schema.Boolean,
392
+ recordsDestroyed: Schema.Number
393
+ });
394
+ const modelsGroup = HttpApiGroup.make("models").annotate(OpenApi.Title, "Models").annotate(OpenApi.Description, "Content model management").add(HttpApiEndpoint.get("listModels", "/models").annotate(OpenApi.Summary, "List all content models").addSuccess(Schema.Array(ModelResponse))).add(HttpApiEndpoint.post("createModel", "/models").annotate(OpenApi.Summary, "Create a new content model").setPayload(CreateModelInput).addSuccess(ModelResponse, { status: 201 })).add(HttpApiEndpoint.get("getModel", "/models/:id").annotate(OpenApi.Summary, "Get a content model by ID or api_key").setPath(Schema.Struct({ id: Schema.String })).addSuccess(ModelWithFieldsResponse)).add(HttpApiEndpoint.patch("updateModel", "/models/:id").annotate(OpenApi.Summary, "Update a content model").setPath(Schema.Struct({ id: Schema.String })).setPayload(UpdateModelInput).addSuccess(ModelResponse)).add(HttpApiEndpoint.del("deleteModel", "/models/:id").annotate(OpenApi.Summary, "Delete a content model").setPath(Schema.Struct({ id: Schema.String })).addSuccess(DeleteModelResponse));
395
+ //#endregion
396
+ //#region src/http/api/fields.ts
397
+ /**
398
+ * HttpApiGroup for field endpoints.
399
+ *
400
+ * Defines the declarative API shape — handlers are implemented separately
401
+ * via HttpApiBuilder.group().
402
+ */
403
+ const fieldsGroup = HttpApiGroup.make("fields").annotate(OpenApi.Title, "Fields").annotate(OpenApi.Description, "Content model field management").add(HttpApiEndpoint.get("listFields", "/models/:modelId/fields").annotate(OpenApi.Summary, "List all fields for a model").setPath(Schema.Struct({ modelId: Schema.String })).addSuccess(Schema.Array(Schema.Unknown))).add(HttpApiEndpoint.post("createField", "/models/:modelId/fields").annotate(OpenApi.Summary, "Create a new field on a model").setPath(Schema.Struct({ modelId: Schema.String })).setPayload(CreateFieldInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.patch("updateField", "/models/:modelId/fields/:fieldId").annotate(OpenApi.Summary, "Update a field").setPath(Schema.Struct({
404
+ modelId: Schema.String,
405
+ fieldId: Schema.String
406
+ })).setPayload(UpdateFieldInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.del("deleteField", "/models/:modelId/fields/:fieldId").annotate(OpenApi.Summary, "Delete a field").setPath(Schema.Struct({
407
+ modelId: Schema.String,
408
+ fieldId: Schema.String
409
+ })).addSuccess(Schema.Unknown));
410
+ //#endregion
411
+ //#region src/http/api/records.ts
412
+ /**
413
+ * HttpApiGroup for record endpoints.
414
+ *
415
+ * Defines the declarative API shape — handlers are implemented separately
416
+ * via HttpApiBuilder.group().
417
+ */
418
+ const ModelApiKeyParams = Schema.Struct({ modelApiKey: Schema.String });
419
+ const IdPath = Schema.Struct({ id: Schema.String });
420
+ const IdVersionPath = Schema.Struct({
421
+ id: Schema.String,
422
+ versionId: Schema.String
423
+ });
424
+ const recordsGroup = HttpApiGroup.make("records").annotate(OpenApi.Title, "Records").annotate(OpenApi.Description, "Content record management").add(HttpApiEndpoint.post("bulkCreateRecords", "/records/bulk").annotate(OpenApi.Summary, "Bulk create records").setPayload(BulkCreateRecordsInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.post("createRecord", "/records").annotate(OpenApi.Summary, "Create a record").setPayload(CreateRecordInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.get("listRecords", "/records").annotate(OpenApi.Summary, "List records for a model").setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.get("listVersions", "/records/:id/versions").annotate(OpenApi.Summary, "List versions for a record").setPath(IdPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.get("getVersion", "/records/:id/versions/:versionId").annotate(OpenApi.Summary, "Get a specific version").setPath(IdVersionPath).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("restoreVersion", "/records/:id/versions/:versionId/restore").annotate(OpenApi.Summary, "Restore a record to a previous version").setPath(IdVersionPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.get("getRecord", "/records/:id").annotate(OpenApi.Summary, "Get a record by ID").setPath(IdPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.patch("updateRecord", "/records/:id").annotate(OpenApi.Summary, "Update a record").setPath(IdPath).setPayload(PatchRecordInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.patch("patchBlocks", "/records/:id/blocks").annotate(OpenApi.Summary, "Patch structured text blocks on a record").setPath(IdPath).setPayload(PatchBlocksInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.del("deleteRecord", "/records/:id").annotate(OpenApi.Summary, "Delete a record").setPath(IdPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("publishRecord", "/records/:id/publish").annotate(OpenApi.Summary, "Publish a record").setPath(IdPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("unpublishRecord", "/records/:id/unpublish").annotate(OpenApi.Summary, "Unpublish a record").setPath(IdPath).setUrlParams(ModelApiKeyParams).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("schedulePublish", "/records/:id/schedule-publish").annotate(OpenApi.Summary, "Schedule a record for publishing").setPath(IdPath).setPayload(ScheduleRecordInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("scheduleUnpublish", "/records/:id/schedule-unpublish").annotate(OpenApi.Summary, "Schedule a record for unpublishing").setPath(IdPath).setPayload(ScheduleRecordInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("clearSchedule", "/records/:id/clear-schedule").annotate(OpenApi.Summary, "Clear scheduled publish/unpublish").setPath(IdPath).setPayload(Schema.Struct({ modelApiKey: Schema.NonEmptyString })).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("reorderRecords", "/reorder").annotate(OpenApi.Summary, "Reorder records within a model").setPayload(ReorderInput).addSuccess(Schema.Unknown));
425
+ //#endregion
426
+ //#region src/http/api/assets.ts
427
+ /**
428
+ * HttpApiGroup for asset endpoints.
429
+ *
430
+ * Defines the declarative API shape — handlers are implemented separately
431
+ * via HttpApiBuilder.group().
432
+ */
433
+ const assetsGroup = HttpApiGroup.make("assets").annotate(OpenApi.Title, "Assets").annotate(OpenApi.Description, "Asset management").add(HttpApiEndpoint.get("listAssets", "/assets").annotate(OpenApi.Summary, "List or search assets").setUrlParams(Schema.Struct({
434
+ q: Schema.optional(Schema.String),
435
+ limit: Schema.optional(Schema.String),
436
+ offset: Schema.optional(Schema.String)
437
+ })).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("createAsset", "/assets").annotate(OpenApi.Summary, "Create a new asset").setPayload(CreateAssetInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.get("getAsset", "/assets/:id").annotate(OpenApi.Summary, "Get an asset by ID").setPath(Schema.Struct({ id: Schema.String })).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.put("replaceAsset", "/assets/:id").annotate(OpenApi.Summary, "Replace an asset").setPath(Schema.Struct({ id: Schema.String })).setPayload(CreateAssetInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.patch("updateAssetMetadata", "/assets/:id").annotate(OpenApi.Summary, "Update asset metadata").setPath(Schema.Struct({ id: Schema.String })).setPayload(UpdateAssetMetadataInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.del("deleteAsset", "/assets/:id").annotate(OpenApi.Summary, "Delete an asset").setPath(Schema.Struct({ id: Schema.String })).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("createUploadUrl", "/assets/upload-url").annotate(OpenApi.Summary, "Create a presigned upload URL").setPayload(CreateUploadUrlInput).addSuccess(Schema.Unknown));
438
+ //#endregion
439
+ //#region src/http/api/locales.ts
440
+ /**
441
+ * HttpApiGroup for locale endpoints.
442
+ *
443
+ * Defines the declarative API shape — handlers are implemented separately
444
+ * via HttpApiBuilder.group().
445
+ */
446
+ const localesGroup = HttpApiGroup.make("locales").annotate(OpenApi.Title, "Locales").annotate(OpenApi.Description, "Locale management").add(HttpApiEndpoint.get("listLocales", "/locales").annotate(OpenApi.Summary, "List all locales").addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("createLocale", "/locales").annotate(OpenApi.Summary, "Create a new locale").setPayload(CreateLocaleInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.del("deleteLocale", "/locales/:id").annotate(OpenApi.Summary, "Delete a locale").setPath(Schema.Struct({ id: Schema.String })).addSuccess(Schema.Unknown));
447
+ //#endregion
448
+ //#region src/http/api/schema-io.ts
449
+ /**
450
+ * HttpApiGroup for schema import/export endpoints.
451
+ *
452
+ * Defines the declarative API shape — handlers are implemented separately
453
+ * via HttpApiBuilder.group().
454
+ */
455
+ const schemaGroup = HttpApiGroup.make("schema").annotate(OpenApi.Title, "Schema").annotate(OpenApi.Description, "Schema import and export").add(HttpApiEndpoint.get("exportSchema", "/schema").annotate(OpenApi.Summary, "Export the full schema").addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("importSchema", "/schema").annotate(OpenApi.Summary, "Import a schema").setPayload(ImportSchemaInput).addSuccess(Schema.Unknown, { status: 201 }));
456
+ //#endregion
457
+ //#region src/http/api/search.ts
458
+ /**
459
+ * HttpApiGroup for search endpoints.
460
+ *
461
+ * Defines the declarative API shape — handlers are implemented separately
462
+ * via HttpApiBuilder.group().
463
+ */
464
+ const searchGroup = HttpApiGroup.make("search").annotate(OpenApi.Title, "Search").annotate(OpenApi.Description, "Full-text and vector search").add(HttpApiEndpoint.post("search", "/search").annotate(OpenApi.Summary, "Search content").setPayload(SearchInput).addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("reindexSearch", "/search/reindex").annotate(OpenApi.Summary, "Reindex search").setPayload(ReindexSearchInput).addSuccess(Schema.Unknown));
465
+ //#endregion
466
+ //#region src/http/api/tokens.ts
467
+ /**
468
+ * HttpApiGroup for editor token endpoints.
469
+ *
470
+ * Defines the declarative API shape — handlers are implemented separately
471
+ * via HttpApiBuilder.group().
472
+ */
473
+ const tokensGroup = HttpApiGroup.make("tokens").annotate(OpenApi.Title, "Tokens").annotate(OpenApi.Description, "Editor token management").add(HttpApiEndpoint.get("listEditorTokens", "/tokens").annotate(OpenApi.Summary, "List all editor tokens").addSuccess(Schema.Unknown)).add(HttpApiEndpoint.post("createEditorToken", "/tokens").annotate(OpenApi.Summary, "Create an editor token").setPayload(CreateEditorTokenInput).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.del("revokeEditorToken", "/tokens/:id").annotate(OpenApi.Summary, "Revoke an editor token").setPath(Schema.Struct({ id: Schema.String })).addSuccess(Schema.Unknown));
474
+ //#endregion
475
+ //#region src/http/api/preview-tokens.ts
476
+ /**
477
+ * HttpApiGroup for preview token endpoints.
478
+ *
479
+ * Defines the declarative API shape — handlers are implemented separately
480
+ * via HttpApiBuilder.group().
481
+ */
482
+ const previewTokensGroup = HttpApiGroup.make("preview-tokens").annotate(OpenApi.Title, "Preview Tokens").annotate(OpenApi.Description, "Preview token creation and validation").add(HttpApiEndpoint.post("createPreviewToken", "/preview-tokens").annotate(OpenApi.Summary, "Create a preview token").setPayload(Schema.Struct({ expiresIn: Schema.optional(Schema.Number) })).addSuccess(Schema.Unknown, { status: 201 })).add(HttpApiEndpoint.get("validatePreviewToken", "/preview-tokens/validate").annotate(OpenApi.Summary, "Validate a preview token").setUrlParams(Schema.Struct({ token: Schema.String })).addSuccess(Schema.Unknown));
483
+ //#endregion
484
+ //#region src/http/api/paths.ts
485
+ /**
486
+ * HttpApiGroup for canonical path resolution endpoints.
487
+ *
488
+ * Defines the declarative API shape — handlers are implemented separately
489
+ * via HttpApiBuilder.group().
490
+ */
491
+ const pathsGroup = HttpApiGroup.make("paths").annotate(OpenApi.Title, "Paths").annotate(OpenApi.Description, "Canonical path resolution").add(HttpApiEndpoint.get("resolveCanonicalPaths", "/paths/:modelApiKey").annotate(OpenApi.Summary, "Resolve canonical paths for a model").setPath(Schema.Struct({ modelApiKey: Schema.String })).addSuccess(Schema.Unknown));
492
+ //#endregion
493
+ //#region src/http/api/index.ts
494
+ /**
495
+ * Declarative HttpApi definition for the agent-cms REST API.
496
+ *
497
+ * Composes all endpoint groups and generates the OpenAPI 3.1.0 spec
498
+ * via `OpenApi.fromApi()`. The spec is served at /openapi.json by
499
+ * the router.
500
+ */
501
+ const cmsApi = HttpApi.make("agent-cms").annotate(OpenApi.Title, "Agent CMS API").annotate(OpenApi.Version, "1.0.0").annotate(OpenApi.Description, "Headless CMS REST API for content management").add(modelsGroup).add(fieldsGroup).add(recordsGroup).add(assetsGroup).add(localesGroup).add(schemaGroup).add(searchGroup).add(tokensGroup).add(previewTokensGroup).add(pathsGroup);
502
+ /** Pre-generated OpenAPI 3.1.0 specification */
503
+ const openApiSpec = OpenApi.fromApi(cmsApi);
504
+ //#endregion
185
505
  //#region src/http/router.ts
186
506
  function describeUnknown(error) {
187
507
  if (error instanceof Error) return `${error.name}: ${error.message}`;
@@ -378,9 +698,19 @@ const tokensRouter = HttpRouter.empty.pipe(HttpRouter.get("/", handle(listEditor
378
698
  })), HttpRouter.del("/:id", Effect.gen(function* () {
379
699
  return yield* handle(revokeEditorToken(param(yield* HttpRouter.params, "id")));
380
700
  })));
701
+ const previewTokensRouter = HttpRouter.empty.pipe(HttpRouter.post("/", Effect.gen(function* () {
702
+ const body = yield* readJsonBody();
703
+ return yield* handle(createPreviewToken(typeof body === "object" && body !== null && "expiresIn" in body ? body.expiresIn : void 0), 201);
704
+ })), HttpRouter.get("/validate", Effect.gen(function* () {
705
+ return yield* handle(validatePreviewToken(yield* queryParam("token")));
706
+ })));
707
+ const pathsRouter = HttpRouter.empty.pipe(HttpRouter.get("/:modelApiKey", Effect.gen(function* () {
708
+ return yield* handle(resolveCanonicalPaths(param(yield* HttpRouter.params, "modelApiKey")));
709
+ })));
381
710
  const setupRouter = HttpRouter.empty.pipe(HttpRouter.post("/setup", handle(ensureSchema().pipe(Effect.as({ ok: true })))));
382
711
  const healthRouter = HttpRouter.empty.pipe(HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })));
383
- const appRouter = HttpRouter.empty.pipe(HttpRouter.concat(healthRouter), HttpRouter.concat(modelsRouter.pipe(HttpRouter.prefixAll("/api/models"))), HttpRouter.concat(fieldsRouter.pipe(HttpRouter.prefixAll("/api"))), HttpRouter.concat(recordsRouter.pipe(HttpRouter.prefixAll("/api"))), HttpRouter.concat(assetsRouter.pipe(HttpRouter.prefixAll("/api/assets"))), HttpRouter.concat(localesRouter.pipe(HttpRouter.prefixAll("/api/locales"))), HttpRouter.concat(schemaRouter.pipe(HttpRouter.prefixAll("/api/schema"))), HttpRouter.concat(searchRouter.pipe(HttpRouter.prefixAll("/api/search"))), HttpRouter.concat(tokensRouter.pipe(HttpRouter.prefixAll("/api/tokens"))), HttpRouter.concat(setupRouter.pipe(HttpRouter.prefixAll("/api"))));
712
+ const openApiRouter = HttpRouter.empty.pipe(HttpRouter.get("/openapi.json", HttpServerResponse.json(openApiSpec)));
713
+ const appRouter = HttpRouter.empty.pipe(HttpRouter.concat(openApiRouter), HttpRouter.concat(healthRouter), HttpRouter.concat(modelsRouter.pipe(HttpRouter.prefixAll("/api/models"))), HttpRouter.concat(fieldsRouter.pipe(HttpRouter.prefixAll("/api"))), HttpRouter.concat(recordsRouter.pipe(HttpRouter.prefixAll("/api"))), HttpRouter.concat(assetsRouter.pipe(HttpRouter.prefixAll("/api/assets"))), HttpRouter.concat(localesRouter.pipe(HttpRouter.prefixAll("/api/locales"))), HttpRouter.concat(schemaRouter.pipe(HttpRouter.prefixAll("/api/schema"))), HttpRouter.concat(searchRouter.pipe(HttpRouter.prefixAll("/api/search"))), HttpRouter.concat(tokensRouter.pipe(HttpRouter.prefixAll("/api/tokens"))), HttpRouter.concat(previewTokensRouter.pipe(HttpRouter.prefixAll("/api/preview-tokens"))), HttpRouter.concat(setupRouter.pipe(HttpRouter.prefixAll("/api"))), HttpRouter.concat(pathsRouter.pipe(HttpRouter.prefixAll("/paths"))));
384
714
  function createWebHandler(sqlLayer, options) {
385
715
  const vectorizeLayer = Layer.succeed(VectorizeContext, options?.ai && options.vectorize ? Option.some({
386
716
  ai: options.ai,
@@ -415,7 +745,7 @@ function createWebHandler(sqlLayer, options) {
415
745
  }
416
746
  async function getGraphqlInstance() {
417
747
  if (!graphqlInstance) {
418
- if (!graphqlModulePromise) graphqlModulePromise = import("./handler-ClOW1ldA.mjs");
748
+ if (!graphqlModulePromise) graphqlModulePromise = import("./handler-B5jgfPOY.mjs");
419
749
  graphqlInstance = (await graphqlModulePromise).createGraphQLHandler(sqlLayer, {
420
750
  assetBaseUrl: options?.assetBaseUrl,
421
751
  isProduction: options?.isProduction
@@ -442,7 +772,7 @@ function createWebHandler(sqlLayer, options) {
442
772
  const headers = new Headers(response.headers);
443
773
  headers.set("Access-Control-Allow-Origin", origin);
444
774
  headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
445
- headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Include-Drafts, X-Exclude-Invalid, X-Filename, X-Requested-With, Accept, User-Agent");
775
+ headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Include-Drafts, X-Exclude-Invalid, X-Filename, X-Requested-With, Accept, User-Agent, X-Preview-Token");
446
776
  headers.set("Access-Control-Max-Age", "600");
447
777
  return new Response(response.body, {
448
778
  status: response.status,
@@ -559,6 +889,10 @@ function createWebHandler(sqlLayer, options) {
559
889
  const h = new Headers(instrumentedRequest.headers);
560
890
  if (credentialType) h.set("X-Credential-Type", credentialType);
561
891
  else h.delete("X-Credential-Type");
892
+ const previewToken = instrumentedRequest.headers.get("X-Preview-Token");
893
+ if (previewToken) try {
894
+ if ((await Effect.runPromise(validatePreviewToken(previewToken).pipe(Effect.provide(fullLayer)))).valid) h.set("X-Include-Drafts", "true");
895
+ } catch {}
562
896
  instrumentedRequest = new Request(instrumentedRequest, { headers: h });
563
897
  } else if (url.pathname === "/mcp") {
564
898
  if ((await getRequestActor(instrumentedRequest))?.type === "editor") return finish(new Response(JSON.stringify({ error: "Unauthorized. Editor tokens must use /mcp/editor." }), {
@@ -582,7 +916,7 @@ function createWebHandler(sqlLayer, options) {
582
916
  headers: { "Content-Type": "application/json" }
583
917
  }));
584
918
  }
585
- } else if (url.pathname.startsWith("/api/")) {
919
+ } else if (url.pathname === "/api/preview-tokens/validate") {} else if (url.pathname.startsWith("/api/")) {
586
920
  const adminOnly = isSchemaMutationRequest(url, instrumentedRequest.method) || url.pathname.startsWith("/api/tokens");
587
921
  const denied = await checkWriteAuth(instrumentedRequest, adminOnly);
588
922
  if (denied) {
@@ -598,36 +932,88 @@ function createWebHandler(sqlLayer, options) {
598
932
  instrumentedRequest = new Request(instrumentedRequest, { headers: h });
599
933
  }
600
934
  if (url.pathname === "/mcp") {
601
- const { createMcpHttpHandler } = await import("./http-transport-DbFCI6Cs.mjs");
935
+ if (options?.loader) {
936
+ const { createMcpHttpHandler } = await import("./http-transport-DVKhbbe1.mjs");
937
+ const adminMcpHandler = mcpHandler ??= createMcpHttpHandler(fullLayer, {
938
+ mode: "admin",
939
+ path: "/mcp",
940
+ r2Bucket: options.r2Bucket,
941
+ assetBaseUrl: options.assetBaseUrl,
942
+ siteUrl: options.siteUrl,
943
+ actor: {
944
+ type: "admin",
945
+ label: "admin"
946
+ }
947
+ });
948
+ const { createCodeModeMcpServer } = await import("./codemode-handler-Bgu2utjN.mjs");
949
+ const { createMcpHandler } = await import("agents/mcp");
950
+ return finish(await createMcpHandler(await createCodeModeMcpServer({
951
+ loader: options.loader,
952
+ mcpHandler: adminMcpHandler
953
+ }), { route: "/mcp" })(instrumentedRequest, {}, {
954
+ waitUntil: () => {},
955
+ passThroughOnException: () => {}
956
+ }));
957
+ }
958
+ const { createMcpHttpHandler } = await import("./http-transport-DVKhbbe1.mjs");
602
959
  const actor = await getRequestActor(instrumentedRequest);
603
960
  return finish(await (actor?.type === "admin" ? mcpHandler ??= createMcpHttpHandler(fullLayer, {
604
961
  mode: "admin",
605
962
  path: "/mcp",
606
963
  r2Bucket: options?.r2Bucket,
607
964
  assetBaseUrl: options?.assetBaseUrl,
965
+ siteUrl: options?.siteUrl,
608
966
  actor
609
967
  }) : createMcpHttpHandler(fullLayer, {
610
968
  mode: "admin",
611
969
  path: "/mcp",
612
970
  r2Bucket: options?.r2Bucket,
613
971
  assetBaseUrl: options?.assetBaseUrl,
972
+ siteUrl: options?.siteUrl,
614
973
  actor
615
974
  }))(instrumentedRequest));
616
975
  }
617
976
  if (url.pathname === "/mcp/editor") {
618
- const { createMcpHttpHandler } = await import("./http-transport-DbFCI6Cs.mjs");
977
+ if (options?.loader) {
978
+ const { createMcpHttpHandler } = await import("./http-transport-DVKhbbe1.mjs");
979
+ const editorMcpHandler = mcpEditorHandler ??= createMcpHttpHandler(fullLayer, {
980
+ mode: "editor",
981
+ path: "/mcp/editor",
982
+ r2Bucket: options.r2Bucket,
983
+ assetBaseUrl: options.assetBaseUrl,
984
+ siteUrl: options.siteUrl,
985
+ actor: {
986
+ type: "editor",
987
+ label: "editor"
988
+ }
989
+ });
990
+ const { createCodeModeMcpServer } = await import("./codemode-handler-Bgu2utjN.mjs");
991
+ const { createMcpHandler } = await import("agents/mcp");
992
+ return finish(await createMcpHandler(await createCodeModeMcpServer({
993
+ loader: options.loader,
994
+ mcpHandler: editorMcpHandler,
995
+ mode: "editor",
996
+ mcpPath: "/mcp/editor"
997
+ }), { route: "/mcp/editor" })(instrumentedRequest, {}, {
998
+ waitUntil: () => {},
999
+ passThroughOnException: () => {}
1000
+ }));
1001
+ }
1002
+ const { createMcpHttpHandler } = await import("./http-transport-DVKhbbe1.mjs");
619
1003
  const actor = await getRequestActor(instrumentedRequest);
620
1004
  return finish(await (actor?.type === "editor" ? createMcpHttpHandler(fullLayer, {
621
1005
  mode: "editor",
622
1006
  path: "/mcp/editor",
623
1007
  r2Bucket: options?.r2Bucket,
624
1008
  assetBaseUrl: options?.assetBaseUrl,
1009
+ siteUrl: options?.siteUrl,
625
1010
  actor
626
1011
  }) : mcpEditorHandler ??= createMcpHttpHandler(fullLayer, {
627
1012
  mode: "editor",
628
1013
  path: "/mcp/editor",
629
1014
  r2Bucket: options?.r2Bucket,
630
1015
  assetBaseUrl: options?.assetBaseUrl,
1016
+ siteUrl: options?.siteUrl,
631
1017
  actor
632
1018
  }))(instrumentedRequest));
633
1019
  }
@@ -642,7 +1028,7 @@ function createWebHandler(sqlLayer, options) {
642
1028
  if (!graphqlModulePromise) {
643
1029
  graphqlImportCache = "miss";
644
1030
  const importStartedAt = performance.now();
645
- graphqlModulePromise = import("./handler-ClOW1ldA.mjs").then((module) => {
1031
+ graphqlModulePromise = import("./handler-B5jgfPOY.mjs").then((module) => {
646
1032
  graphqlImportMs = Number((performance.now() - importStartedAt).toFixed(3));
647
1033
  return module;
648
1034
  });
@@ -740,7 +1126,10 @@ function createWebHandler(sqlLayer, options) {
740
1126
  async execute(query, variables, context) {
741
1127
  return (await getGraphqlInstance()).execute(query, variables, context);
742
1128
  },
743
- runScheduledTransitions: runScheduledTransitions$1
1129
+ runScheduledTransitions: runScheduledTransitions$1,
1130
+ resolveCanonicalPaths(modelApiKey) {
1131
+ return Effect.runPromise(resolveCanonicalPaths(modelApiKey).pipe(Effect.provide(fullLayer)));
1132
+ }
744
1133
  };
745
1134
  }
746
1135
  //#endregion
@@ -766,7 +1155,9 @@ const RawCmsBindingsSchema = Schema.Struct({
766
1155
  r2AccessKeyId: OptionalNonEmptyString,
767
1156
  r2SecretAccessKey: OptionalNonEmptyString,
768
1157
  r2BucketName: OptionalNonEmptyString,
769
- cfAccountId: OptionalNonEmptyString
1158
+ cfAccountId: OptionalNonEmptyString,
1159
+ siteUrl: Schema.optional(Schema.String),
1160
+ loader: Schema.optional(RuntimeObject)
770
1161
  }).pipe(Schema.filter((bindings) => {
771
1162
  return bindings.ai !== void 0 === (bindings.vectorize !== void 0);
772
1163
  }, { message: () => "ai and vectorize bindings must be configured together" }), Schema.filter((bindings) => {
@@ -799,7 +1190,9 @@ function decodeCmsBindings(input) {
799
1190
  secretAccessKey: bindings.r2SecretAccessKey,
800
1191
  bucketName: bindings.r2BucketName,
801
1192
  accountId: bindings.cfAccountId
802
- } : void 0
1193
+ } : void 0,
1194
+ siteUrl: bindings.siteUrl,
1195
+ loader: bindings.loader
803
1196
  };
804
1197
  }
805
1198
  //#endregion
@@ -1103,7 +1496,8 @@ function cacheKey(bindings, hooks) {
1103
1496
  bindings.writeKey ?? "",
1104
1497
  bindings.r2Credentials?.accessKeyId ?? "",
1105
1498
  bindings.r2Credentials?.bucketName ?? "",
1106
- bindings.r2Credentials?.accountId ?? ""
1499
+ bindings.r2Credentials?.accountId ?? "",
1500
+ bindings.siteUrl ?? ""
1107
1501
  ].join("|");
1108
1502
  }
1109
1503
  /**
@@ -1156,12 +1550,15 @@ function createCMSHandlerUncached(bindings, hooks) {
1156
1550
  ai: bindings.ai,
1157
1551
  vectorize: bindings.vectorize,
1158
1552
  hooks,
1159
- r2Credentials: bindings.r2Credentials
1553
+ r2Credentials: bindings.r2Credentials,
1554
+ siteUrl: bindings.siteUrl,
1555
+ loader: bindings.loader
1160
1556
  });
1161
1557
  return {
1162
1558
  fetch: (request) => webHandler.fetch(request),
1163
1559
  execute: webHandler.execute,
1164
- runScheduledTransitions: (now) => webHandler.runScheduledTransitions(now)
1560
+ runScheduledTransitions: (now) => webHandler.runScheduledTransitions(now),
1561
+ resolveCanonicalPaths: (modelApiKey) => webHandler.resolveCanonicalPaths(modelApiKey)
1165
1562
  };
1166
1563
  }
1167
1564
  //#endregion