agent-cms 0.1.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 ADDED
@@ -0,0 +1,1170 @@
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";
3
+ import { D1Client } from "@effect/sql-d1";
4
+ import { Cause, Effect, Layer, Logger, Option, ParseResult, Schema } from "effect";
5
+ import { HttpApp, HttpRouter, HttpServerError, HttpServerRequest, HttpServerResponse } from "@effect/platform";
6
+ import { SqlClient } from "@effect/sql";
7
+ //#region src/migrations.ts
8
+ /**
9
+ * Embedded schema migrations — runs automatically on first request.
10
+ * The project is still pre-deployment, so the full schema lives in a single genesis migration.
11
+ */
12
+ const MIGRATIONS = [{
13
+ version: 1,
14
+ statements: [
15
+ `CREATE TABLE IF NOT EXISTS "assets" (
16
+ "id" text PRIMARY KEY,
17
+ "filename" text NOT NULL,
18
+ "mime_type" text NOT NULL,
19
+ "size" integer NOT NULL,
20
+ "width" integer,
21
+ "height" integer,
22
+ "alt" text,
23
+ "title" text,
24
+ "r2_key" text NOT NULL,
25
+ "blurhash" text,
26
+ "colors" text,
27
+ "focal_point" text,
28
+ "tags" text DEFAULT '[]',
29
+ "custom_data" text DEFAULT '{}',
30
+ "created_at" text NOT NULL,
31
+ "updated_at" text NOT NULL,
32
+ "created_by" text,
33
+ "updated_by" text
34
+ )`,
35
+ `CREATE TABLE IF NOT EXISTS "models" (
36
+ "id" text PRIMARY KEY,
37
+ "name" text NOT NULL,
38
+ "api_key" text NOT NULL UNIQUE,
39
+ "is_block" integer DEFAULT false NOT NULL,
40
+ "singleton" integer DEFAULT false NOT NULL,
41
+ "sortable" integer DEFAULT false NOT NULL,
42
+ "tree" integer DEFAULT false NOT NULL,
43
+ "has_draft" integer DEFAULT true NOT NULL,
44
+ "all_locales_required" integer DEFAULT 0 NOT NULL,
45
+ "ordering" text,
46
+ "created_at" text NOT NULL,
47
+ "updated_at" text NOT NULL
48
+ )`,
49
+ `CREATE TABLE IF NOT EXISTS "fieldsets" (
50
+ "id" text PRIMARY KEY,
51
+ "model_id" text NOT NULL,
52
+ "title" text NOT NULL,
53
+ "position" integer DEFAULT 0 NOT NULL,
54
+ CONSTRAINT "fk_fieldsets_model_id_models_id_fk" FOREIGN KEY ("model_id") REFERENCES "models"("id") ON DELETE CASCADE
55
+ )`,
56
+ `CREATE TABLE IF NOT EXISTS "fields" (
57
+ "id" text PRIMARY KEY,
58
+ "model_id" text NOT NULL,
59
+ "label" text NOT NULL,
60
+ "api_key" text NOT NULL,
61
+ "field_type" text NOT NULL,
62
+ "position" integer DEFAULT 0 NOT NULL,
63
+ "localized" integer DEFAULT false NOT NULL,
64
+ "validators" text DEFAULT '{}',
65
+ "default_value" text,
66
+ "appearance" text,
67
+ "hint" text,
68
+ "fieldset_id" text,
69
+ "created_at" text NOT NULL,
70
+ "updated_at" text NOT NULL,
71
+ CONSTRAINT "fk_fields_model_id_models_id_fk" FOREIGN KEY ("model_id") REFERENCES "models"("id") ON DELETE CASCADE,
72
+ CONSTRAINT "fk_fields_fieldset_id_fieldsets_id_fk" FOREIGN KEY ("fieldset_id") REFERENCES "fieldsets"("id") ON DELETE SET NULL
73
+ )`,
74
+ `CREATE TABLE IF NOT EXISTS "locales" (
75
+ "id" text PRIMARY KEY,
76
+ "code" text NOT NULL UNIQUE,
77
+ "position" integer DEFAULT 0 NOT NULL,
78
+ "fallback_locale_id" text,
79
+ CONSTRAINT "fk_locales_fallback_locale_id_locales_id_fk" FOREIGN KEY ("fallback_locale_id") REFERENCES "locales"("id") ON DELETE SET NULL
80
+ )`,
81
+ `CREATE TABLE IF NOT EXISTS "site_settings" (
82
+ "id" text PRIMARY KEY DEFAULT 'default',
83
+ "site_name" text,
84
+ "title_suffix" text,
85
+ "no_index" integer DEFAULT 0 NOT NULL,
86
+ "favicon_id" text,
87
+ "facebook_page_url" text,
88
+ "twitter_account" text,
89
+ "fallback_seo_title" text,
90
+ "fallback_seo_description" text,
91
+ "fallback_seo_image_id" text,
92
+ "fallback_seo_twitter_card" text DEFAULT 'summary',
93
+ "updated_at" text NOT NULL DEFAULT (datetime('now')),
94
+ CONSTRAINT "fk_site_settings_favicon" FOREIGN KEY ("favicon_id") REFERENCES "assets"("id") ON DELETE SET NULL,
95
+ CONSTRAINT "fk_site_settings_seo_image" FOREIGN KEY ("fallback_seo_image_id") REFERENCES "assets"("id") ON DELETE SET NULL
96
+ )`,
97
+ `CREATE TABLE IF NOT EXISTS "record_versions" (
98
+ "id" text PRIMARY KEY,
99
+ "model_api_key" text NOT NULL,
100
+ "record_id" text NOT NULL,
101
+ "version_number" integer NOT NULL,
102
+ "snapshot" text NOT NULL,
103
+ "action" text NOT NULL DEFAULT 'publish',
104
+ "actor_type" text,
105
+ "actor_label" text,
106
+ "actor_token_id" text,
107
+ "created_at" text NOT NULL
108
+ )`,
109
+ `CREATE INDEX IF NOT EXISTS "idx_record_versions_lookup"
110
+ ON "record_versions" ("model_api_key", "record_id", "version_number" DESC)`,
111
+ `CREATE TABLE IF NOT EXISTS "editor_tokens" (
112
+ "id" TEXT PRIMARY KEY,
113
+ "name" TEXT NOT NULL,
114
+ "token_prefix" TEXT NOT NULL,
115
+ "secret_hash" TEXT NOT NULL,
116
+ "created_at" TEXT NOT NULL DEFAULT (datetime('now')),
117
+ "last_used_at" TEXT,
118
+ "expires_at" TEXT
119
+ )`,
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)`,
138
+ `CREATE INDEX IF NOT EXISTS "idx_assets_basename" ON "assets" ("basename")`,
139
+ `CREATE INDEX IF NOT EXISTS "idx_assets_format" ON "assets" ("format")`
140
+ ]
141
+ }];
142
+ /**
143
+ * Ensure all CMS system tables exist.
144
+ * Uses a _cms_migrations tracking table. Idempotent — safe to call on every request.
145
+ * Fast path: single SELECT after first run.
146
+ */
147
+ function ensureSchema() {
148
+ return Effect.gen(function* () {
149
+ const sql = yield* SqlClient.SqlClient;
150
+ yield* sql.unsafe(`CREATE TABLE IF NOT EXISTS "_cms_migrations" ("version" integer PRIMARY KEY, "applied_at" text NOT NULL DEFAULT (datetime('now')))`);
151
+ const applied = yield* sql.unsafe("SELECT version FROM _cms_migrations");
152
+ const appliedSet = new Set(applied.map((r) => r.version));
153
+ for (const migration of MIGRATIONS) {
154
+ if (appliedSet.has(migration.version)) continue;
155
+ for (const stmt of migration.statements) yield* sql.unsafe(stmt);
156
+ yield* sql.unsafe("INSERT INTO _cms_migrations (version) VALUES (?)", [migration.version]);
157
+ }
158
+ });
159
+ }
160
+ //#endregion
161
+ //#region src/attribution.ts
162
+ const ACTOR_TYPE_HEADER = "X-Cms-Actor-Type";
163
+ const ACTOR_LABEL_HEADER = "X-Cms-Actor-Label";
164
+ const ACTOR_TOKEN_ID_HEADER = "X-Cms-Actor-Token-Id";
165
+ function actorHeaders(actor) {
166
+ if (!actor) return {};
167
+ const headers = {
168
+ [ACTOR_TYPE_HEADER]: actor.type,
169
+ [ACTOR_LABEL_HEADER]: actor.label
170
+ };
171
+ if (actor.tokenId) headers[ACTOR_TOKEN_ID_HEADER] = actor.tokenId;
172
+ return headers;
173
+ }
174
+ function actorFromHeaders(headers) {
175
+ const type = headers.get(ACTOR_TYPE_HEADER);
176
+ const label = headers.get(ACTOR_LABEL_HEADER);
177
+ if (type !== "admin" && type !== "editor" || !label) return null;
178
+ return {
179
+ type,
180
+ label,
181
+ tokenId: headers.get(ACTOR_TOKEN_ID_HEADER)
182
+ };
183
+ }
184
+ //#endregion
185
+ //#region src/http/router.ts
186
+ function describeUnknown(error) {
187
+ if (error instanceof Error) return `${error.name}: ${error.message}`;
188
+ if (typeof error === "string") return error;
189
+ try {
190
+ return JSON.stringify(error);
191
+ } catch {
192
+ return String(error);
193
+ }
194
+ }
195
+ function getRequestIdFromHeaders(headers) {
196
+ return headers.get("x-request-id") ?? headers.get("cf-ray") ?? crypto.randomUUID();
197
+ }
198
+ /** Helper: run a CMS Effect and return an HTTP response */
199
+ function handle(effect, status = 200) {
200
+ return effect.pipe(Effect.flatMap((result) => HttpServerResponse.json(result, { status })), Effect.tapErrorCause((cause) => Effect.logError("REST effect failed", Cause.pretty(cause))), Effect.catchAll((error) => {
201
+ if (isCmsError(error)) {
202
+ const mapped = errorToResponse(error);
203
+ return HttpServerResponse.json(mapped.body, { status: mapped.status });
204
+ }
205
+ return Effect.logError("Unhandled REST error").pipe(Effect.annotateLogs({ error: describeUnknown(error) }), Effect.zipRight(HttpServerResponse.json({ error: "Internal server error" }, { status: 500 })));
206
+ }), Effect.catchAllDefect((defect) => {
207
+ return Effect.logError("REST defect").pipe(Effect.annotateLogs({ defect: describeUnknown(defect) }), Effect.zipRight(HttpServerResponse.json({ error: "Internal server error" }, { status: 500 })));
208
+ }));
209
+ }
210
+ /** Extract a required path parameter, defaulting to empty string if missing */
211
+ function param(params, name) {
212
+ return params[name] ?? "";
213
+ }
214
+ /** Get query param */
215
+ function queryParam(name) {
216
+ return Effect.gen(function* () {
217
+ const req = yield* HttpServerRequest.HttpServerRequest;
218
+ return new URL(req.url, "http://localhost").searchParams.get(name) ?? "";
219
+ });
220
+ }
221
+ function decodeUnknownInput(schema, input, message = "Invalid input") {
222
+ return Schema.decodeUnknown(schema)(input).pipe(Effect.mapError((e) => new ValidationError({ message: `${message}: ${e.message}` })));
223
+ }
224
+ function readJsonBody(message = "Invalid JSON body") {
225
+ return Effect.gen(function* () {
226
+ return yield* (yield* HttpServerRequest.HttpServerRequest).json.pipe(Effect.mapError((e) => new ValidationError({ message: `${message}: ${describeUnknown(e)}` })));
227
+ });
228
+ }
229
+ function currentActor() {
230
+ return Effect.gen(function* () {
231
+ const req = yield* HttpServerRequest.HttpServerRequest;
232
+ return actorFromHeaders(new Headers(req.headers));
233
+ });
234
+ }
235
+ const modelsRouter = HttpRouter.empty.pipe(HttpRouter.get("/", handle(listModels())), HttpRouter.post("/", Effect.gen(function* () {
236
+ return yield* handle(createModel(yield* decodeUnknownInput(CreateModelInput, yield* readJsonBody())), 201);
237
+ })), HttpRouter.get("/:id", Effect.gen(function* () {
238
+ return yield* handle(getModel(param(yield* HttpRouter.params, "id")));
239
+ })), HttpRouter.patch("/:id", Effect.gen(function* () {
240
+ const params = yield* HttpRouter.params;
241
+ const input = yield* decodeUnknownInput(UpdateModelInput, yield* readJsonBody());
242
+ return yield* handle(updateModel(param(params, "id"), input));
243
+ })), HttpRouter.del("/:id", Effect.gen(function* () {
244
+ return yield* handle(deleteModel(param(yield* HttpRouter.params, "id")));
245
+ })));
246
+ const fieldsRouter = HttpRouter.empty.pipe(HttpRouter.get("/models/:modelId/fields", Effect.gen(function* () {
247
+ return yield* handle(listFields(param(yield* HttpRouter.params, "modelId")));
248
+ })), HttpRouter.post("/models/:modelId/fields", Effect.gen(function* () {
249
+ const params = yield* HttpRouter.params;
250
+ const input = yield* decodeUnknownInput(CreateFieldInput, yield* readJsonBody());
251
+ return yield* handle(createField(param(params, "modelId"), input), 201);
252
+ })), HttpRouter.patch("/models/:modelId/fields/:fieldId", Effect.gen(function* () {
253
+ const params = yield* HttpRouter.params;
254
+ const input = yield* decodeUnknownInput(UpdateFieldInput, yield* readJsonBody());
255
+ return yield* handle(updateField(param(params, "fieldId"), input));
256
+ })), HttpRouter.del("/models/:modelId/fields/:fieldId", Effect.gen(function* () {
257
+ return yield* handle(deleteField(param(yield* HttpRouter.params, "fieldId")));
258
+ })));
259
+ const recordsRouter = HttpRouter.empty.pipe(HttpRouter.post("/records/bulk", Effect.gen(function* () {
260
+ return yield* handle(bulkCreateRecords(yield* decodeUnknownInput(BulkCreateRecordsInput, yield* readJsonBody()), yield* currentActor()), 201);
261
+ })), HttpRouter.post("/records", Effect.gen(function* () {
262
+ return yield* handle(createRecord(yield* decodeUnknownInput(CreateRecordInput, yield* readJsonBody()), yield* currentActor()), 201);
263
+ })), HttpRouter.get("/records", Effect.gen(function* () {
264
+ return yield* handle(listRecords(yield* queryParam("modelApiKey")));
265
+ })), HttpRouter.get("/records/:id/versions", Effect.gen(function* () {
266
+ const params = yield* HttpRouter.params;
267
+ return yield* handle(listVersions(yield* queryParam("modelApiKey"), param(params, "id")));
268
+ })), HttpRouter.get("/records/:id/versions/:versionId", Effect.gen(function* () {
269
+ return yield* handle(getVersion(param(yield* HttpRouter.params, "versionId")));
270
+ })), HttpRouter.post("/records/:id/versions/:versionId/restore", Effect.gen(function* () {
271
+ const params = yield* HttpRouter.params;
272
+ const modelApiKey = yield* queryParam("modelApiKey");
273
+ const actor = yield* currentActor();
274
+ return yield* handle(restoreVersion(modelApiKey, param(params, "id"), param(params, "versionId"), actor));
275
+ })), HttpRouter.get("/records/:id", Effect.gen(function* () {
276
+ const params = yield* HttpRouter.params;
277
+ return yield* handle(getRecord(yield* queryParam("modelApiKey"), param(params, "id")));
278
+ })), HttpRouter.patch("/records/:id", Effect.gen(function* () {
279
+ const params = yield* HttpRouter.params;
280
+ const input = yield* decodeUnknownInput(PatchRecordInput, yield* readJsonBody());
281
+ const actor = yield* currentActor();
282
+ return yield* handle(patchRecord(param(params, "id"), input, actor));
283
+ })), HttpRouter.patch("/records/:id/blocks", Effect.gen(function* () {
284
+ const params = yield* HttpRouter.params;
285
+ const body = yield* readJsonBody();
286
+ return yield* handle(patchBlocksForField(yield* decodeUnknownInput(PatchBlocksInput, typeof body === "object" && body !== null ? {
287
+ ...body,
288
+ recordId: param(params, "id")
289
+ } : { recordId: param(params, "id") }), yield* currentActor()));
290
+ })), HttpRouter.del("/records/:id", Effect.gen(function* () {
291
+ const params = yield* HttpRouter.params;
292
+ return yield* handle(removeRecord(yield* queryParam("modelApiKey"), param(params, "id")));
293
+ })), HttpRouter.post("/records/:id/publish", Effect.gen(function* () {
294
+ const params = yield* HttpRouter.params;
295
+ const modelApiKey = yield* queryParam("modelApiKey");
296
+ const actor = yield* currentActor();
297
+ return yield* handle(publishRecord(modelApiKey, param(params, "id"), actor));
298
+ })), HttpRouter.post("/records/:id/unpublish", Effect.gen(function* () {
299
+ const params = yield* HttpRouter.params;
300
+ const modelApiKey = yield* queryParam("modelApiKey");
301
+ const actor = yield* currentActor();
302
+ return yield* handle(unpublishRecord(modelApiKey, param(params, "id"), actor));
303
+ })), HttpRouter.post("/records/:id/schedule-publish", Effect.gen(function* () {
304
+ const params = yield* HttpRouter.params;
305
+ const input = yield* decodeUnknownInput(ScheduleRecordInput, yield* readJsonBody());
306
+ const actor = yield* currentActor();
307
+ return yield* handle(schedulePublish(input.modelApiKey, param(params, "id"), input.at, actor));
308
+ })), HttpRouter.post("/records/:id/schedule-unpublish", Effect.gen(function* () {
309
+ const params = yield* HttpRouter.params;
310
+ const input = yield* decodeUnknownInput(ScheduleRecordInput, yield* readJsonBody());
311
+ const actor = yield* currentActor();
312
+ return yield* handle(scheduleUnpublish(input.modelApiKey, param(params, "id"), input.at, actor));
313
+ })), HttpRouter.post("/records/:id/clear-schedule", Effect.gen(function* () {
314
+ const params = yield* HttpRouter.params;
315
+ const body = yield* readJsonBody();
316
+ const input = yield* decodeUnknownInput(Schema.Struct({ modelApiKey: Schema.NonEmptyString }), body);
317
+ const actor = yield* currentActor();
318
+ return yield* handle(clearSchedule(input.modelApiKey, param(params, "id"), actor));
319
+ })), HttpRouter.post("/reorder", Effect.gen(function* () {
320
+ const rawBody = yield* readJsonBody();
321
+ return yield* handle(Effect.gen(function* () {
322
+ const { modelApiKey, recordIds } = yield* decodeUnknownInput(ReorderInput, rawBody);
323
+ return yield* reorderRecords(modelApiKey, recordIds, yield* currentActor());
324
+ }));
325
+ })));
326
+ const assetsRouter = HttpRouter.empty.pipe(HttpRouter.get("/", Effect.gen(function* () {
327
+ const req = yield* HttpServerRequest.HttpServerRequest;
328
+ const url = new URL(req.url, "http://localhost");
329
+ const q = url.searchParams.get("q");
330
+ const limit = url.searchParams.get("limit");
331
+ const offset = url.searchParams.get("offset");
332
+ if (q !== null || limit !== null || offset !== null) {
333
+ const parsed = yield* decodeUnknownInput(SearchAssetsInput, {
334
+ query: q ?? void 0,
335
+ limit: limit !== null ? Number(limit) : void 0,
336
+ offset: offset !== null ? Number(offset) : void 0
337
+ }, "Invalid asset search input");
338
+ return yield* handle(searchAssets({
339
+ query: parsed.query,
340
+ limit: Math.min(parsed.limit, 100),
341
+ offset: parsed.offset
342
+ }));
343
+ }
344
+ return yield* handle(listAssets());
345
+ })), HttpRouter.post("/", Effect.gen(function* () {
346
+ return yield* handle(createAsset(yield* decodeUnknownInput(CreateAssetInput, yield* readJsonBody()), yield* currentActor()), 201);
347
+ })), HttpRouter.get("/:id", Effect.gen(function* () {
348
+ return yield* handle(getAsset(param(yield* HttpRouter.params, "id")));
349
+ })), HttpRouter.put("/:id", Effect.gen(function* () {
350
+ const params = yield* HttpRouter.params;
351
+ const input = yield* decodeUnknownInput(CreateAssetInput, yield* readJsonBody());
352
+ const actor = yield* currentActor();
353
+ return yield* handle(replaceAsset(param(params, "id"), input, actor));
354
+ })), HttpRouter.patch("/:id", Effect.gen(function* () {
355
+ const params = yield* HttpRouter.params;
356
+ const input = yield* decodeUnknownInput(UpdateAssetMetadataInput, yield* readJsonBody());
357
+ const actor = yield* currentActor();
358
+ return yield* handle(updateAssetMetadata(param(params, "id"), input, actor));
359
+ })), HttpRouter.del("/:id", Effect.gen(function* () {
360
+ return yield* handle(deleteAsset(param(yield* HttpRouter.params, "id")));
361
+ })));
362
+ const localesRouter = HttpRouter.empty.pipe(HttpRouter.get("/", handle(listLocales())), HttpRouter.post("/", Effect.gen(function* () {
363
+ return yield* handle(createLocale(yield* decodeUnknownInput(CreateLocaleInput, yield* readJsonBody())), 201);
364
+ })), HttpRouter.del("/:id", Effect.gen(function* () {
365
+ return yield* handle(deleteLocale(param(yield* HttpRouter.params, "id")));
366
+ })));
367
+ const schemaRouter = HttpRouter.empty.pipe(HttpRouter.get("/", handle(exportSchema())), HttpRouter.post("/", Effect.gen(function* () {
368
+ return yield* handle(importSchema(yield* decodeUnknownInput(ImportSchemaInput, yield* readJsonBody())), 201);
369
+ })));
370
+ const searchRouter = HttpRouter.empty.pipe(HttpRouter.post("/", Effect.gen(function* () {
371
+ return yield* handle(search(yield* decodeUnknownInput(SearchInput, yield* readJsonBody(), "Invalid search input")));
372
+ })), HttpRouter.post("/reindex", Effect.gen(function* () {
373
+ const modelApiKey = (yield* decodeUnknownInput(ReindexSearchInput, yield* readJsonBody())).modelApiKey;
374
+ return yield* handle(reindexAll(modelApiKey));
375
+ })));
376
+ const tokensRouter = HttpRouter.empty.pipe(HttpRouter.get("/", handle(listEditorTokens())), HttpRouter.post("/", Effect.gen(function* () {
377
+ return yield* handle(createEditorToken(yield* decodeUnknownInput(CreateEditorTokenInput, yield* readJsonBody())), 201);
378
+ })), HttpRouter.del("/:id", Effect.gen(function* () {
379
+ return yield* handle(revokeEditorToken(param(yield* HttpRouter.params, "id")));
380
+ })));
381
+ const setupRouter = HttpRouter.empty.pipe(HttpRouter.post("/setup", handle(ensureSchema().pipe(Effect.as({ ok: true })))));
382
+ 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"))));
384
+ function createWebHandler(sqlLayer, options) {
385
+ const vectorizeLayer = Layer.succeed(VectorizeContext, options?.ai && options.vectorize ? Option.some({
386
+ ai: options.ai,
387
+ vectorize: options.vectorize
388
+ }) : Option.none());
389
+ const hooksLayer = Layer.succeed(HooksContext, options?.hooks ? Option.some(options.hooks) : Option.none());
390
+ const fullLayer = Layer.mergeAll(sqlLayer, vectorizeLayer, hooksLayer, Logger.json);
391
+ const runLoggedEffect = (effect) => {
392
+ Effect.runFork(effect.pipe(Effect.provide(fullLayer), Effect.orDie));
393
+ };
394
+ const logInfo = (message, fields) => {
395
+ runLoggedEffect(Effect.logInfo(message).pipe(Effect.annotateLogs(fields)));
396
+ };
397
+ const logError = (message, fields) => {
398
+ runLoggedEffect(Effect.logError(message).pipe(Effect.annotateLogs(fields)));
399
+ };
400
+ const restApp = Effect.flatten(HttpRouter.toHttpApp(appRouter)).pipe(Effect.catchAll((error) => {
401
+ if (isCmsError(error)) {
402
+ const mapped = errorToResponse(error);
403
+ return HttpServerResponse.json(mapped.body, { status: mapped.status });
404
+ }
405
+ if (error instanceof HttpServerError.RouteNotFound) return HttpServerResponse.json({ error: "Not found" }, { status: 404 });
406
+ return Effect.logError("REST handler error").pipe(Effect.annotateLogs({ error: describeUnknown(error) }), Effect.zipRight(HttpServerResponse.json({ error: "Internal server error" }, { status: 500 })));
407
+ }), Effect.provide(fullLayer));
408
+ const restHandler = HttpApp.toWebHandler(restApp);
409
+ let graphqlInstance = null;
410
+ let mcpHandler = null;
411
+ let graphqlModulePromise = null;
412
+ let mcpEditorHandler = null;
413
+ function invalidateGraphqlSchemaCache() {
414
+ if (graphqlInstance) graphqlInstance.invalidateSchema();
415
+ }
416
+ async function getGraphqlInstance() {
417
+ if (!graphqlInstance) {
418
+ if (!graphqlModulePromise) graphqlModulePromise = import("./handler-ClOW1ldA.mjs");
419
+ graphqlInstance = (await graphqlModulePromise).createGraphQLHandler(sqlLayer, {
420
+ assetBaseUrl: options?.assetBaseUrl,
421
+ isProduction: options?.isProduction
422
+ });
423
+ }
424
+ return graphqlInstance;
425
+ }
426
+ async function runScheduledTransitions$1(now = /* @__PURE__ */ new Date()) {
427
+ const result = await Effect.runPromise(runScheduledTransitions(now).pipe(Effect.withSpan("schedule.run_transitions"), Effect.annotateLogs({ now: now.toISOString() }), Effect.provide(fullLayer)));
428
+ if (result.published.length > 0 || result.unpublished.length > 0) invalidateGraphqlSchemaCache();
429
+ return result;
430
+ }
431
+ function isSchemaMutationRequest(url, method) {
432
+ if (![
433
+ "POST",
434
+ "PATCH",
435
+ "DELETE"
436
+ ].includes(method)) return false;
437
+ return url.pathname.startsWith("/api/models") || url.pathname.startsWith("/api/locales") || url.pathname.startsWith("/api/schema") || url.pathname === "/api/setup";
438
+ }
439
+ /** Add CORS headers to a response */
440
+ function withCors(response, request) {
441
+ const origin = request.headers.get("Origin") ?? "*";
442
+ const headers = new Headers(response.headers);
443
+ headers.set("Access-Control-Allow-Origin", origin);
444
+ 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");
446
+ headers.set("Access-Control-Max-Age", "600");
447
+ return new Response(response.body, {
448
+ status: response.status,
449
+ statusText: response.statusText,
450
+ headers
451
+ });
452
+ }
453
+ /**
454
+ * Extract Bearer token from Authorization header.
455
+ * Accepts: "Bearer <token>" or raw "<token>"
456
+ */
457
+ function getBearerToken(request) {
458
+ const header = request.headers.get("Authorization");
459
+ if (!header) return null;
460
+ if (header.startsWith("Bearer ")) return header.slice(7);
461
+ return header;
462
+ }
463
+ /**
464
+ * Check if a request is authorized for write access.
465
+ * If no writeKey is configured, all requests are allowed (local dev).
466
+ * When adminOnly is true, only writeKey is accepted (not editor tokens).
467
+ */
468
+ async function checkWriteAuth(request, adminOnly = false) {
469
+ if (!options?.writeKey) return null;
470
+ const token = getBearerToken(request);
471
+ if (token === options.writeKey) return null;
472
+ if (adminOnly) return new UnauthorizedError({ message: "Unauthorized. This endpoint requires admin (writeKey) access." });
473
+ if (token && token.startsWith("etk_")) try {
474
+ await Effect.runPromise(validateEditorToken(token).pipe(Effect.provide(fullLayer)));
475
+ return null;
476
+ } catch {
477
+ return new UnauthorizedError({ message: "Unauthorized. Invalid or expired editor token." });
478
+ }
479
+ return new UnauthorizedError({ message: "Unauthorized. Provide a valid write API key or editor token via Authorization: Bearer <key>" });
480
+ }
481
+ async function getRequestActor(request) {
482
+ if (!options?.writeKey) return {
483
+ type: "admin",
484
+ label: "admin"
485
+ };
486
+ const token = getBearerToken(request);
487
+ if (token === options.writeKey) return {
488
+ type: "admin",
489
+ label: "admin"
490
+ };
491
+ if (token && token.startsWith("etk_")) try {
492
+ const editorToken = await Effect.runPromise(validateEditorToken(token).pipe(Effect.provide(fullLayer)));
493
+ return {
494
+ type: "editor",
495
+ label: editorToken.name,
496
+ tokenId: editorToken.id
497
+ };
498
+ } catch {
499
+ return null;
500
+ }
501
+ return null;
502
+ }
503
+ async function getCredentialType(request) {
504
+ return (await getRequestActor(request))?.type ?? null;
505
+ }
506
+ const fetchHandler = async (request) => {
507
+ const requestId = getRequestIdFromHeaders(request.headers);
508
+ const headers = new Headers(request.headers);
509
+ headers.set("x-request-id", requestId);
510
+ let instrumentedRequest = new Request(request, { headers });
511
+ const startedAt = Date.now();
512
+ const finish = (response) => {
513
+ const corsResponse = withCors(response, instrumentedRequest);
514
+ const responseHeaders = new Headers(corsResponse.headers);
515
+ responseHeaders.set("x-request-id", requestId);
516
+ const wrapped = new Response(corsResponse.body, {
517
+ status: corsResponse.status,
518
+ statusText: corsResponse.statusText,
519
+ headers: responseHeaders
520
+ });
521
+ const durationMs = Date.now() - startedAt;
522
+ if (wrapped.status >= 500 || instrumentedRequest.url.includes("/api/assets/")) {
523
+ const logFields = {
524
+ requestId,
525
+ method: instrumentedRequest.method,
526
+ path: new URL(instrumentedRequest.url).pathname,
527
+ status: wrapped.status,
528
+ durationMs
529
+ };
530
+ if (wrapped.status >= 500) logError("worker request completed", logFields);
531
+ else logInfo("worker request completed", logFields);
532
+ }
533
+ return wrapped;
534
+ };
535
+ try {
536
+ if (instrumentedRequest.method === "OPTIONS") return finish(new Response(null, { status: 204 }));
537
+ const url = new URL(instrumentedRequest.url);
538
+ if (url.pathname.startsWith("/assets/") && options?.r2Bucket) {
539
+ const assetId = url.pathname.replace("/assets/", "").split("/")[0];
540
+ if (assetId) {
541
+ const r2Key = await Effect.runPromise(Effect.gen(function* () {
542
+ return (yield* (yield* SqlClient.SqlClient).unsafe("SELECT r2_key FROM assets WHERE id = ?", [assetId]))[0]?.r2_key ?? null;
543
+ }).pipe(Effect.provide(fullLayer), Effect.orDie));
544
+ if (r2Key) {
545
+ const object = await options.r2Bucket.get(r2Key);
546
+ if (object) {
547
+ const headers = new Headers();
548
+ headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream");
549
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
550
+ return finish(new Response(object.body, { headers }));
551
+ }
552
+ }
553
+ }
554
+ return finish(new Response("Not found", { status: 404 }));
555
+ }
556
+ if (url.pathname === "/health") return finish(new Response(JSON.stringify({ status: "ok" }), { headers: { "Content-Type": "application/json" } }));
557
+ if (url.pathname === "/graphql") {
558
+ const credentialType = await getCredentialType(instrumentedRequest);
559
+ const h = new Headers(instrumentedRequest.headers);
560
+ if (credentialType) h.set("X-Credential-Type", credentialType);
561
+ else h.delete("X-Credential-Type");
562
+ instrumentedRequest = new Request(instrumentedRequest, { headers: h });
563
+ } else if (url.pathname === "/mcp") {
564
+ if ((await getRequestActor(instrumentedRequest))?.type === "editor") return finish(new Response(JSON.stringify({ error: "Unauthorized. Editor tokens must use /mcp/editor." }), {
565
+ status: 401,
566
+ headers: { "Content-Type": "application/json" }
567
+ }));
568
+ const denied = await checkWriteAuth(instrumentedRequest, true);
569
+ if (denied) {
570
+ const mapped = errorToResponse(denied);
571
+ return finish(new Response(JSON.stringify(mapped.body), {
572
+ status: mapped.status,
573
+ headers: { "Content-Type": "application/json" }
574
+ }));
575
+ }
576
+ } else if (url.pathname === "/mcp/editor") {
577
+ const denied = await checkWriteAuth(instrumentedRequest, false);
578
+ if (denied) {
579
+ const mapped = errorToResponse(denied);
580
+ return finish(new Response(JSON.stringify(mapped.body), {
581
+ status: mapped.status,
582
+ headers: { "Content-Type": "application/json" }
583
+ }));
584
+ }
585
+ } else if (url.pathname.startsWith("/api/")) {
586
+ const adminOnly = isSchemaMutationRequest(url, instrumentedRequest.method) || url.pathname.startsWith("/api/tokens");
587
+ const denied = await checkWriteAuth(instrumentedRequest, adminOnly);
588
+ if (denied) {
589
+ const mapped = errorToResponse(denied);
590
+ return finish(new Response(JSON.stringify(mapped.body), {
591
+ status: mapped.status,
592
+ headers: { "Content-Type": "application/json" }
593
+ }));
594
+ }
595
+ const actor = await getRequestActor(instrumentedRequest);
596
+ const h = new Headers(instrumentedRequest.headers);
597
+ for (const [key, value] of Object.entries(actorHeaders(actor))) h.set(key, value);
598
+ instrumentedRequest = new Request(instrumentedRequest, { headers: h });
599
+ }
600
+ if (url.pathname === "/mcp") {
601
+ const { createMcpHttpHandler } = await import("./http-transport-DbFCI6Cs.mjs");
602
+ const actor = await getRequestActor(instrumentedRequest);
603
+ return finish(await (actor?.type === "admin" ? mcpHandler ??= createMcpHttpHandler(fullLayer, {
604
+ mode: "admin",
605
+ path: "/mcp",
606
+ r2Bucket: options?.r2Bucket,
607
+ assetBaseUrl: options?.assetBaseUrl,
608
+ actor
609
+ }) : createMcpHttpHandler(fullLayer, {
610
+ mode: "admin",
611
+ path: "/mcp",
612
+ r2Bucket: options?.r2Bucket,
613
+ assetBaseUrl: options?.assetBaseUrl,
614
+ actor
615
+ }))(instrumentedRequest));
616
+ }
617
+ if (url.pathname === "/mcp/editor") {
618
+ const { createMcpHttpHandler } = await import("./http-transport-DbFCI6Cs.mjs");
619
+ const actor = await getRequestActor(instrumentedRequest);
620
+ return finish(await (actor?.type === "editor" ? createMcpHttpHandler(fullLayer, {
621
+ mode: "editor",
622
+ path: "/mcp/editor",
623
+ r2Bucket: options?.r2Bucket,
624
+ assetBaseUrl: options?.assetBaseUrl,
625
+ actor
626
+ }) : mcpEditorHandler ??= createMcpHttpHandler(fullLayer, {
627
+ mode: "editor",
628
+ path: "/mcp/editor",
629
+ r2Bucket: options?.r2Bucket,
630
+ assetBaseUrl: options?.assetBaseUrl,
631
+ actor
632
+ }))(instrumentedRequest));
633
+ }
634
+ if (url.pathname === "/graphql") {
635
+ const traceEnabled = instrumentedRequest.headers.get("X-Bench-Trace") === "1" || instrumentedRequest.headers.get("X-Debug-Sql") === "true";
636
+ let graphqlImportMs = 0;
637
+ let graphqlInitMs = 0;
638
+ let graphqlImportCache = "hit";
639
+ let graphqlInitCache = "hit";
640
+ if (!graphqlInstance) {
641
+ graphqlInitCache = "miss";
642
+ if (!graphqlModulePromise) {
643
+ graphqlImportCache = "miss";
644
+ const importStartedAt = performance.now();
645
+ graphqlModulePromise = import("./handler-ClOW1ldA.mjs").then((module) => {
646
+ graphqlImportMs = Number((performance.now() - importStartedAt).toFixed(3));
647
+ return module;
648
+ });
649
+ }
650
+ const module = await graphqlModulePromise;
651
+ const initStartedAt = performance.now();
652
+ graphqlInstance = module.createGraphQLHandler(sqlLayer, {
653
+ assetBaseUrl: options?.assetBaseUrl,
654
+ isProduction: options?.isProduction
655
+ });
656
+ graphqlInitMs = Number((performance.now() - initStartedAt).toFixed(3));
657
+ }
658
+ const response = await graphqlInstance.handle(instrumentedRequest);
659
+ if (!traceEnabled) return finish(response);
660
+ const headers = new Headers(response.headers);
661
+ headers.set("X-Cms-Graphql-Import-Ms", graphqlImportMs.toFixed(3));
662
+ headers.set("X-Cms-Graphql-Import-Cache", graphqlImportCache);
663
+ headers.set("X-Cms-Graphql-Init-Ms", graphqlInitMs.toFixed(3));
664
+ headers.set("X-Cms-Graphql-Init-Cache", graphqlInitCache);
665
+ return finish(new Response(response.body, {
666
+ status: response.status,
667
+ statusText: response.statusText,
668
+ headers
669
+ }));
670
+ }
671
+ if (url.pathname === "/api/assets/upload-url" && instrumentedRequest.method === "POST") {
672
+ if (!options?.r2Credentials) return finish(new Response(JSON.stringify({ error: "Presigned uploads not configured" }), {
673
+ status: 501,
674
+ headers: { "Content-Type": "application/json" }
675
+ }));
676
+ const body = await instrumentedRequest.json();
677
+ const parsed = Schema.decodeUnknownSync(CreateUploadUrlInput)(body);
678
+ const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
679
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
680
+ const creds = options.r2Credentials;
681
+ const assetId = crypto.randomUUID();
682
+ const r2Key = `uploads/${assetId}/${parsed.filename}`;
683
+ const uploadUrl = await getSignedUrl(new S3Client({
684
+ region: "auto",
685
+ endpoint: `https://${creds.accountId}.r2.cloudflarestorage.com`,
686
+ credentials: {
687
+ accessKeyId: creds.accessKeyId,
688
+ secretAccessKey: creds.secretAccessKey
689
+ }
690
+ }), new PutObjectCommand({
691
+ Bucket: creds.bucketName,
692
+ Key: r2Key,
693
+ ContentType: parsed.mimeType
694
+ }), { expiresIn: 3600 });
695
+ return finish(new Response(JSON.stringify({
696
+ uploadUrl,
697
+ r2Key,
698
+ assetId
699
+ }), {
700
+ status: 200,
701
+ headers: { "Content-Type": "application/json" }
702
+ }));
703
+ }
704
+ if (url.pathname.match(/^\/api\/assets\/[^/]+\/file$/) && instrumentedRequest.method === "PUT") {
705
+ if (!options?.r2Bucket) return finish(new Response(JSON.stringify({ error: "R2 bucket not configured" }), {
706
+ status: 501,
707
+ headers: { "Content-Type": "application/json" }
708
+ }));
709
+ const assetId = url.pathname.split("/")[3];
710
+ const contentType = instrumentedRequest.headers.get("Content-Type") ?? "application/octet-stream";
711
+ const r2Key = `uploads/${assetId}/${instrumentedRequest.headers.get("X-Filename") ?? "upload"}`;
712
+ const body = await instrumentedRequest.arrayBuffer();
713
+ await options.r2Bucket.put(r2Key, body, { httpMetadata: { contentType } });
714
+ return finish(new Response(JSON.stringify({
715
+ r2Key,
716
+ assetId
717
+ }), {
718
+ status: 200,
719
+ headers: { "Content-Type": "application/json" }
720
+ }));
721
+ }
722
+ const response = await restHandler(instrumentedRequest);
723
+ if (response.status < 400 && isSchemaMutationRequest(url, instrumentedRequest.method)) invalidateGraphqlSchemaCache();
724
+ return finish(response);
725
+ } catch (error) {
726
+ logError("worker request crashed", {
727
+ requestId,
728
+ method: instrumentedRequest.method,
729
+ path: new URL(instrumentedRequest.url).pathname,
730
+ error: describeUnknown(error)
731
+ });
732
+ return finish(new Response(JSON.stringify({ error: "Internal server error" }), {
733
+ status: 500,
734
+ headers: { "Content-Type": "application/json" }
735
+ }));
736
+ }
737
+ };
738
+ return {
739
+ fetch: fetchHandler,
740
+ async execute(query, variables, context) {
741
+ return (await getGraphqlInstance()).execute(query, variables, context);
742
+ },
743
+ runScheduledTransitions: runScheduledTransitions$1
744
+ };
745
+ }
746
+ //#endregion
747
+ //#region src/config-schema.ts
748
+ const RuntimeObject = Schema.Unknown.pipe(Schema.filter((value) => typeof value === "object" && value !== null, { message: () => "Expected runtime binding object" }));
749
+ const OptionalNonEmptyString = Schema.optional(Schema.NonEmptyTrimmedString);
750
+ const AssetBaseUrl = Schema.String.pipe(Schema.filter((value) => {
751
+ try {
752
+ new URL(value);
753
+ return true;
754
+ } catch {
755
+ return false;
756
+ }
757
+ }, { message: () => "assetBaseUrl must be a valid URL" }));
758
+ const RawCmsBindingsSchema = Schema.Struct({
759
+ db: RuntimeObject,
760
+ assets: Schema.optional(RuntimeObject),
761
+ environment: Schema.optional(Schema.Literal("production", "development")),
762
+ assetBaseUrl: Schema.optional(AssetBaseUrl),
763
+ writeKey: OptionalNonEmptyString,
764
+ ai: Schema.optional(RuntimeObject),
765
+ vectorize: Schema.optional(RuntimeObject),
766
+ r2AccessKeyId: OptionalNonEmptyString,
767
+ r2SecretAccessKey: OptionalNonEmptyString,
768
+ r2BucketName: OptionalNonEmptyString,
769
+ cfAccountId: OptionalNonEmptyString
770
+ }).pipe(Schema.filter((bindings) => {
771
+ return bindings.ai !== void 0 === (bindings.vectorize !== void 0);
772
+ }, { message: () => "ai and vectorize bindings must be configured together" }), Schema.filter((bindings) => {
773
+ const r2Fields = [
774
+ bindings.r2AccessKeyId,
775
+ bindings.r2SecretAccessKey,
776
+ bindings.r2BucketName,
777
+ bindings.cfAccountId
778
+ ];
779
+ const presentCount = r2Fields.filter((value) => value !== void 0).length;
780
+ return presentCount === 0 || presentCount === r2Fields.length;
781
+ }, { message: () => "R2 credentials must include r2AccessKeyId, r2SecretAccessKey, r2BucketName, and cfAccountId together" }));
782
+ function formatConfigParseError(error) {
783
+ return ParseResult.ArrayFormatter.formatErrorSync(error).map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
784
+ }
785
+ function decodeCmsBindings(input) {
786
+ const decoded = Schema.decodeUnknownEither(RawCmsBindingsSchema)(input);
787
+ if (decoded._tag === "Left") throw new Error(`Invalid CMS bindings: ${formatConfigParseError(decoded.left)}`);
788
+ const bindings = decoded.right;
789
+ return {
790
+ db: bindings.db,
791
+ assets: bindings.assets,
792
+ environment: bindings.environment,
793
+ assetBaseUrl: bindings.assetBaseUrl,
794
+ writeKey: bindings.writeKey,
795
+ ai: bindings.ai,
796
+ vectorize: bindings.vectorize,
797
+ r2Credentials: bindings.r2AccessKeyId ? {
798
+ accessKeyId: bindings.r2AccessKeyId,
799
+ secretAccessKey: bindings.r2SecretAccessKey,
800
+ bucketName: bindings.r2BucketName,
801
+ accountId: bindings.cfAccountId
802
+ } : void 0
803
+ };
804
+ }
805
+ //#endregion
806
+ //#region src/admin-client.ts
807
+ function trimTrailingSlash$1(input) {
808
+ return input.replace(/\/$/, "");
809
+ }
810
+ async function readJsonOrError(response) {
811
+ if ((response.headers.get("content-type") ?? "").includes("application/json")) return response.json();
812
+ return { error: await response.text() };
813
+ }
814
+ function createCmsAdminClient(config) {
815
+ const endpoint = trimTrailingSlash$1(config.endpoint);
816
+ const fetchFn = config.fetch ?? globalThis.fetch;
817
+ async function request(path, init) {
818
+ const headers = new Headers(init?.headers);
819
+ headers.set("Authorization", `Bearer ${config.writeKey}`);
820
+ if (init?.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
821
+ const response = await fetchFn(`${endpoint}${path}`, {
822
+ ...init,
823
+ headers
824
+ });
825
+ if (!response.ok) {
826
+ const errorPayload = await readJsonOrError(response);
827
+ const message = typeof errorPayload === "object" && errorPayload !== null && "error" in errorPayload ? String(errorPayload.error) : `Request failed with status ${response.status}`;
828
+ throw new Error(message);
829
+ }
830
+ return response;
831
+ }
832
+ return {
833
+ async createEditorToken(input) {
834
+ return (await request("/api/tokens", {
835
+ method: "POST",
836
+ body: JSON.stringify(input)
837
+ })).json();
838
+ },
839
+ async listEditorTokens() {
840
+ return (await request("/api/tokens")).json();
841
+ },
842
+ async revokeEditorToken(id) {
843
+ return (await request(`/api/tokens/${id}`, { method: "DELETE" })).json();
844
+ }
845
+ };
846
+ }
847
+ //#endregion
848
+ //#region src/editor-mcp-proxy.ts
849
+ function trimTrailingSlash(input) {
850
+ return input.replace(/\/$/, "");
851
+ }
852
+ function ensureLeadingSlash(input) {
853
+ return input.startsWith("/") ? input : `/${input}`;
854
+ }
855
+ function base64UrlEncodeBytes(bytes) {
856
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
857
+ }
858
+ function base64UrlEncodeText(input) {
859
+ return base64UrlEncodeBytes(new TextEncoder().encode(input));
860
+ }
861
+ function base64UrlDecodeText(input) {
862
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
863
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
864
+ const decoded = atob(padded);
865
+ const bytes = Uint8Array.from(decoded, (char) => char.charCodeAt(0));
866
+ return new TextDecoder().decode(bytes);
867
+ }
868
+ async function importHmacKey(secret) {
869
+ return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
870
+ name: "HMAC",
871
+ hash: "SHA-256"
872
+ }, false, ["sign", "verify"]);
873
+ }
874
+ async function signJwt(secret, claims) {
875
+ const key = await importHmacKey(secret);
876
+ const message = `${base64UrlEncodeText(JSON.stringify({
877
+ alg: "HS256",
878
+ typ: "JWT"
879
+ }))}.${base64UrlEncodeText(JSON.stringify(claims))}`;
880
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
881
+ return `${message}.${base64UrlEncodeBytes(new Uint8Array(signature))}`;
882
+ }
883
+ async function verifyJwt(secret, token) {
884
+ const [header, payload, signature] = token.split(".");
885
+ if (!header || !payload || !signature) return null;
886
+ const key = await importHmacKey(secret);
887
+ const message = `${header}.${payload}`;
888
+ const signatureBytes = Uint8Array.from(atob(signature.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - signature.length % 4) % 4)), (char) => char.charCodeAt(0));
889
+ if (!await crypto.subtle.verify("HMAC", key, signatureBytes, new TextEncoder().encode(message))) return null;
890
+ const claims = JSON.parse(base64UrlDecodeText(payload));
891
+ if (claims.exp <= Math.floor(Date.now() / 1e3)) return null;
892
+ return claims;
893
+ }
894
+ async function sha256Base64Url(input) {
895
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
896
+ return base64UrlEncodeBytes(new Uint8Array(digest));
897
+ }
898
+ function jsonResponse(body, status = 200) {
899
+ return new Response(JSON.stringify(body), {
900
+ status,
901
+ headers: { "Content-Type": "application/json" }
902
+ });
903
+ }
904
+ function redirectResponse(url) {
905
+ return new Response(null, {
906
+ status: 302,
907
+ headers: { Location: url.toString() }
908
+ });
909
+ }
910
+ function unauthorizedForMcp(resourceMetadataUrl) {
911
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
912
+ status: 401,
913
+ headers: {
914
+ "Content-Type": "application/json",
915
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
916
+ }
917
+ });
918
+ }
919
+ function cloneHeadersWithoutAuthorization(headers) {
920
+ const cloned = new Headers(headers);
921
+ cloned.delete("authorization");
922
+ return cloned;
923
+ }
924
+ function parseBearerToken(request) {
925
+ const header = request.headers.get("authorization");
926
+ if (!header) return null;
927
+ return header.startsWith("Bearer ") ? header.slice(7) : header;
928
+ }
929
+ function appendPathToUrl(base, path) {
930
+ return `${trimTrailingSlash(base)}${ensureLeadingSlash(path)}`;
931
+ }
932
+ function getPathname(request) {
933
+ return new URL(request.url).pathname;
934
+ }
935
+ function createEditorMcpProxy(config) {
936
+ const appBaseUrl = trimTrailingSlash(config.appBaseUrl);
937
+ const cmsBaseUrl = trimTrailingSlash(config.cmsBaseUrl);
938
+ const mountPath = ensureLeadingSlash(config.mountPath ?? "/editor-access");
939
+ const fetchFn = config.fetch ?? globalThis.fetch;
940
+ const oauthTokenTtlSeconds = config.oauthTokenTtlSeconds ?? 3600;
941
+ const cmsTokenTtlSeconds = config.cmsTokenTtlSeconds ?? 3600;
942
+ const issuer = appendPathToUrl(appBaseUrl, mountPath);
943
+ const paths = {
944
+ mountPath,
945
+ issuer,
946
+ mcpPath: `${mountPath}/mcp`,
947
+ authorizationPath: `${mountPath}/authorize`,
948
+ tokenPath: `${mountPath}/token`,
949
+ registrationPath: `${mountPath}/register`,
950
+ oauthAuthorizationServerMetadataPath: `/.well-known/oauth-authorization-server${mountPath}`,
951
+ protectedResourceMetadataPath: `/.well-known/oauth-protected-resource${mountPath}/mcp`
952
+ };
953
+ const resourceUrl = appendPathToUrl(appBaseUrl, paths.mcpPath);
954
+ const protectedResourceMetadataUrl = appendPathToUrl(appBaseUrl, paths.protectedResourceMetadataPath);
955
+ const cmsAdmin = createCmsAdminClient({
956
+ endpoint: cmsBaseUrl,
957
+ writeKey: config.cmsWriteKey,
958
+ fetch: fetchFn
959
+ });
960
+ async function handleAuthorize(request) {
961
+ const editor = await config.getEditor(request);
962
+ if (!editor) {
963
+ const loginUrl = new URL(config.getLoginUrl(request), appBaseUrl);
964
+ loginUrl.searchParams.set("returnTo", request.url);
965
+ return redirectResponse(loginUrl);
966
+ }
967
+ const url = new URL(request.url);
968
+ const responseType = url.searchParams.get("response_type");
969
+ const clientId = url.searchParams.get("client_id");
970
+ const redirectUri = url.searchParams.get("redirect_uri");
971
+ const codeChallenge = url.searchParams.get("code_challenge");
972
+ const codeChallengeMethod = url.searchParams.get("code_challenge_method");
973
+ const state = url.searchParams.get("state");
974
+ const resource = url.searchParams.get("resource") ?? resourceUrl;
975
+ const scope = url.searchParams.get("scope") ?? "editor:mcp";
976
+ if (responseType !== "code" || !clientId || !redirectUri || !codeChallenge || codeChallengeMethod !== "S256") return jsonResponse({ error: "Invalid authorization request" }, 400);
977
+ const cmsToken = await cmsAdmin.createEditorToken({
978
+ name: editor.name,
979
+ expiresIn: cmsTokenTtlSeconds
980
+ });
981
+ const code = await signJwt(config.oauthSecret, {
982
+ sub: editor.id,
983
+ name: editor.name,
984
+ client_id: clientId,
985
+ redirect_uri: redirectUri,
986
+ code_challenge: codeChallenge,
987
+ code_challenge_method: "S256",
988
+ resource,
989
+ scope,
990
+ cms_token: cmsToken.token,
991
+ exp: Math.floor(Date.now() / 1e3) + 120
992
+ });
993
+ const redirectUrl = new URL(redirectUri);
994
+ redirectUrl.searchParams.set("code", code);
995
+ if (state) redirectUrl.searchParams.set("state", state);
996
+ return redirectResponse(redirectUrl);
997
+ }
998
+ async function handleToken(request) {
999
+ const body = await request.formData();
1000
+ const grantType = body.get("grant_type");
1001
+ const code = body.get("code");
1002
+ const clientId = body.get("client_id");
1003
+ const redirectUri = body.get("redirect_uri");
1004
+ const codeVerifier = body.get("code_verifier");
1005
+ if (grantType !== "authorization_code" || typeof code !== "string" || typeof clientId !== "string" || typeof redirectUri !== "string" || typeof codeVerifier !== "string") return jsonResponse({ error: "invalid_request" }, 400);
1006
+ const claims = await verifyJwt(config.oauthSecret, code);
1007
+ if (!claims || claims.client_id !== clientId || claims.redirect_uri !== redirectUri || await sha256Base64Url(codeVerifier) !== claims.code_challenge) return jsonResponse({ error: "invalid_grant" }, 400);
1008
+ return jsonResponse({
1009
+ access_token: await signJwt(config.oauthSecret, {
1010
+ sub: claims.sub,
1011
+ name: claims.name,
1012
+ resource: claims.resource,
1013
+ scope: claims.scope,
1014
+ cms_token: claims.cms_token,
1015
+ exp: Math.floor(Date.now() / 1e3) + oauthTokenTtlSeconds
1016
+ }),
1017
+ token_type: "Bearer",
1018
+ expires_in: oauthTokenTtlSeconds,
1019
+ scope: claims.scope
1020
+ });
1021
+ }
1022
+ async function handleRegister(request) {
1023
+ const metadata = await request.json();
1024
+ const clientId = metadata.client_id || `editor-mcp-${crypto.randomUUID()}`;
1025
+ return jsonResponse({
1026
+ ...metadata,
1027
+ client_id: clientId,
1028
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
1029
+ token_endpoint_auth_method: "none",
1030
+ grant_types: ["authorization_code"],
1031
+ response_types: ["code"]
1032
+ });
1033
+ }
1034
+ async function handleMcp(request) {
1035
+ const accessToken = parseBearerToken(request);
1036
+ if (!accessToken) return unauthorizedForMcp(protectedResourceMetadataUrl);
1037
+ const claims = await verifyJwt(config.oauthSecret, accessToken);
1038
+ if (!claims || claims.resource !== resourceUrl) return unauthorizedForMcp(protectedResourceMetadataUrl);
1039
+ const upstreamUrl = new URL(cmsBaseUrl + "/mcp/editor");
1040
+ const upstreamRequest = new Request(upstreamUrl, {
1041
+ method: request.method,
1042
+ headers: cloneHeadersWithoutAuthorization(request.headers),
1043
+ body: request.body,
1044
+ duplex: "half",
1045
+ redirect: "manual"
1046
+ });
1047
+ upstreamRequest.headers.set("Authorization", `Bearer ${claims.cms_token}`);
1048
+ return fetchFn(upstreamRequest);
1049
+ }
1050
+ async function handle(request) {
1051
+ const pathname = getPathname(request);
1052
+ if (pathname === paths.oauthAuthorizationServerMetadataPath) return jsonResponse({
1053
+ issuer,
1054
+ authorization_endpoint: appendPathToUrl(appBaseUrl, paths.authorizationPath),
1055
+ token_endpoint: appendPathToUrl(appBaseUrl, paths.tokenPath),
1056
+ registration_endpoint: appendPathToUrl(appBaseUrl, paths.registrationPath),
1057
+ response_types_supported: ["code"],
1058
+ grant_types_supported: ["authorization_code"],
1059
+ token_endpoint_auth_methods_supported: ["none"],
1060
+ code_challenge_methods_supported: ["S256"],
1061
+ scopes_supported: ["editor:mcp"]
1062
+ });
1063
+ if (pathname === paths.protectedResourceMetadataPath) return jsonResponse({
1064
+ resource: resourceUrl,
1065
+ authorization_servers: [issuer],
1066
+ bearer_methods_supported: ["header"],
1067
+ scopes_supported: ["editor:mcp"],
1068
+ resource_name: config.resourceName ?? "agent-cms editor MCP proxy"
1069
+ });
1070
+ if (pathname === paths.authorizationPath) return handleAuthorize(request);
1071
+ if (pathname === paths.tokenPath && request.method === "POST") return handleToken(request);
1072
+ if (pathname === paths.registrationPath && request.method === "POST") return handleRegister(request);
1073
+ if (pathname === paths.mcpPath) return handleMcp(request);
1074
+ return new Response("Not found", { status: 404 });
1075
+ }
1076
+ return {
1077
+ paths,
1078
+ fetch: handle
1079
+ };
1080
+ }
1081
+ //#endregion
1082
+ //#region src/index.ts
1083
+ const objectIds = /* @__PURE__ */ new WeakMap();
1084
+ let nextObjectId = 1;
1085
+ const handlerCache = /* @__PURE__ */ new Map();
1086
+ function getObjectId(value) {
1087
+ if (!value) return 0;
1088
+ const existing = objectIds.get(value);
1089
+ if (existing) return existing;
1090
+ const id = nextObjectId++;
1091
+ objectIds.set(value, id);
1092
+ return id;
1093
+ }
1094
+ function cacheKey(bindings, hooks) {
1095
+ return [
1096
+ getObjectId(bindings.db),
1097
+ getObjectId(bindings.assets),
1098
+ getObjectId(bindings.ai),
1099
+ getObjectId(bindings.vectorize),
1100
+ getObjectId(hooks),
1101
+ bindings.environment ?? "",
1102
+ bindings.assetBaseUrl ?? "",
1103
+ bindings.writeKey ?? "",
1104
+ bindings.r2Credentials?.accessKeyId ?? "",
1105
+ bindings.r2Credentials?.bucketName ?? "",
1106
+ bindings.r2Credentials?.accountId ?? ""
1107
+ ].join("|");
1108
+ }
1109
+ /**
1110
+ * Create the agent-cms fetch handler.
1111
+ *
1112
+ * Usage in your Worker's src/index.ts:
1113
+ * ```typescript
1114
+ * import { createCMSHandler } from "agent-cms";
1115
+ *
1116
+ * export default {
1117
+ * fetch: (request, env) => getHandler(env).fetch(request),
1118
+ * scheduled: (_controller, env) => getHandler(env).runScheduledTransitions(),
1119
+ * };
1120
+ *
1121
+ * let cachedHandler: ReturnType<typeof createCMSHandler> | null = null;
1122
+ *
1123
+ * function getHandler(env: Env) {
1124
+ * if (!cachedHandler) {
1125
+ * cachedHandler = createCMSHandler({
1126
+ * bindings: {
1127
+ * db: env.DB,
1128
+ * assets: env.ASSETS,
1129
+ * environment: env.ENVIRONMENT,
1130
+ * assetBaseUrl: env.ASSET_BASE_URL,
1131
+ * writeKey: env.CMS_WRITE_KEY,
1132
+ * ai: env.AI,
1133
+ * vectorize: env.VECTORIZE,
1134
+ * },
1135
+ * });
1136
+ * }
1137
+ * return cachedHandler;
1138
+ * }
1139
+ * ```
1140
+ */
1141
+ function createCMSHandler(config) {
1142
+ const decodedBindings = decodeCmsBindings(config.bindings);
1143
+ const key = cacheKey(decodedBindings, config.hooks);
1144
+ const cached = handlerCache.get(key);
1145
+ if (cached) return cached;
1146
+ const handler = createCMSHandlerUncached(decodedBindings, config.hooks);
1147
+ handlerCache.set(key, handler);
1148
+ return handler;
1149
+ }
1150
+ function createCMSHandlerUncached(bindings, hooks) {
1151
+ const webHandler = createWebHandler(D1Client.layer({ db: bindings.db }).pipe(Layer.orDie), {
1152
+ assetBaseUrl: bindings.assetBaseUrl,
1153
+ isProduction: bindings.environment === "production",
1154
+ writeKey: bindings.writeKey,
1155
+ r2Bucket: bindings.assets,
1156
+ ai: bindings.ai,
1157
+ vectorize: bindings.vectorize,
1158
+ hooks,
1159
+ r2Credentials: bindings.r2Credentials
1160
+ });
1161
+ return {
1162
+ fetch: (request) => webHandler.fetch(request),
1163
+ execute: webHandler.execute,
1164
+ runScheduledTransitions: (now) => webHandler.runScheduledTransitions(now)
1165
+ };
1166
+ }
1167
+ //#endregion
1168
+ export { createCMSHandler, createCmsAdminClient, createEditorMcpProxy };
1169
+
1170
+ //# sourceMappingURL=index.mjs.map