@webmaster-droid/server 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,509 @@
1
+ import {
2
+ getBearerToken,
3
+ normalizeEditablePath,
4
+ verifyAdminToken
5
+ } from "./chunk-SIXK4BMG.js";
6
+ import {
7
+ runAgentTurn
8
+ } from "./chunk-PS4GESOZ.js";
9
+ import {
10
+ CmsService
11
+ } from "./chunk-EYY23AAK.js";
12
+ import {
13
+ SupabaseCmsStorage
14
+ } from "./chunk-OWXROQ4O.js";
15
+
16
+ // src/api-supabase/http.ts
17
+ var CORS_HEADERS = {
18
+ "access-control-allow-origin": "*",
19
+ "access-control-allow-headers": "content-type,authorization,accept,cache-control",
20
+ "access-control-allow-methods": "GET,POST,OPTIONS"
21
+ };
22
+ var SSE_BASE_HEADERS = {
23
+ ...CORS_HEADERS,
24
+ "content-type": "text/event-stream",
25
+ "cache-control": "no-cache, no-transform",
26
+ connection: "keep-alive"
27
+ };
28
+ function jsonResponse(statusCode, body, headers) {
29
+ return new Response(JSON.stringify(body), {
30
+ status: statusCode,
31
+ headers: {
32
+ ...CORS_HEADERS,
33
+ "content-type": "application/json",
34
+ ...headers
35
+ }
36
+ });
37
+ }
38
+ function sseHeaders(headers) {
39
+ return {
40
+ ...SSE_BASE_HEADERS,
41
+ ...headers
42
+ };
43
+ }
44
+ function sseEvent(event, data) {
45
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
46
+ return `event: ${event}
47
+ data: ${payload}
48
+
49
+ `;
50
+ }
51
+ async function parseJsonBody(request) {
52
+ const raw = await request.text();
53
+ if (!raw) {
54
+ throw new Error("Request body is required.");
55
+ }
56
+ return JSON.parse(raw);
57
+ }
58
+ function normalizePath(path) {
59
+ return path.replace(/\/+$/, "") || "/";
60
+ }
61
+
62
+ // src/api-supabase/service-factory.ts
63
+ import {
64
+ createDefaultCmsDocument
65
+ } from "@webmaster-droid/contracts";
66
+ var DEFAULT_SUPABASE_BUCKET = "webmaster-droid-cms";
67
+ var DEFAULT_STORAGE_PREFIX = "cms";
68
+ var servicePromise = null;
69
+ function requireEnv(name) {
70
+ const value = process.env[name];
71
+ if (!value) {
72
+ throw new Error(`Missing required environment variable: ${name}`);
73
+ }
74
+ return value;
75
+ }
76
+ function parseBooleanEnv(name, defaultValue) {
77
+ const raw = process.env[name];
78
+ if (!raw) {
79
+ return defaultValue;
80
+ }
81
+ return raw.toLowerCase() === "true";
82
+ }
83
+ function parseOptionalEnv(name) {
84
+ const raw = process.env[name];
85
+ if (!raw) {
86
+ return void 0;
87
+ }
88
+ const trimmed = raw.trim();
89
+ return trimmed || void 0;
90
+ }
91
+ function buildModelConfig() {
92
+ return {
93
+ openaiEnabled: parseBooleanEnv("MODEL_OPENAI_ENABLED", true),
94
+ geminiEnabled: parseBooleanEnv("MODEL_GEMINI_ENABLED", true),
95
+ defaultModelId: process.env.DEFAULT_MODEL_ID ?? "openai:gpt-5.2"
96
+ };
97
+ }
98
+ function normalizeAllowedPath(path) {
99
+ const trimmed = path.trim();
100
+ if (!trimmed.startsWith("/")) {
101
+ return null;
102
+ }
103
+ if (trimmed === "/") {
104
+ return "/";
105
+ }
106
+ const normalized = trimmed.replace(/\/+$/, "");
107
+ if (!normalized) {
108
+ return null;
109
+ }
110
+ return `${normalized}/`;
111
+ }
112
+ function parseAllowedInternalPathsEnv() {
113
+ const raw = process.env.CMS_ALLOWED_INTERNAL_PATHS;
114
+ if (!raw) {
115
+ return ["/"];
116
+ }
117
+ const normalized = raw.split(",").map((item) => normalizeAllowedPath(item)).filter((value) => Boolean(value));
118
+ return normalized.length > 0 ? normalized : ["/"];
119
+ }
120
+ function resolveStorageBucket() {
121
+ return parseOptionalEnv("CMS_SUPABASE_BUCKET") ?? parseOptionalEnv("SUPABASE_STORAGE_BUCKET") ?? DEFAULT_SUPABASE_BUCKET;
122
+ }
123
+ function resolveStoragePrefix() {
124
+ return parseOptionalEnv("CMS_STORAGE_PREFIX") ?? DEFAULT_STORAGE_PREFIX;
125
+ }
126
+ function deriveSupabasePublicBaseUrl(bucket) {
127
+ const explicit = parseOptionalEnv("CMS_PUBLIC_BASE_URL");
128
+ if (explicit) {
129
+ return explicit;
130
+ }
131
+ const supabaseUrl = parseOptionalEnv("SUPABASE_URL");
132
+ if (!supabaseUrl) {
133
+ return void 0;
134
+ }
135
+ const normalized = supabaseUrl.replace(/\/+$/, "");
136
+ return `${normalized}/storage/v1/object/public/${bucket}`;
137
+ }
138
+ async function getCmsService() {
139
+ if (!servicePromise) {
140
+ servicePromise = (async () => {
141
+ const bucket = resolveStorageBucket();
142
+ const storage = new SupabaseCmsStorage({
143
+ supabaseUrl: requireEnv("SUPABASE_URL"),
144
+ serviceRoleKey: requireEnv("SUPABASE_SERVICE_ROLE_KEY"),
145
+ bucket,
146
+ prefix: resolveStoragePrefix()
147
+ });
148
+ const service = new CmsService(storage, {
149
+ modelConfig: buildModelConfig(),
150
+ allowedInternalPaths: parseAllowedInternalPathsEnv(),
151
+ publicAssetBaseUrl: deriveSupabasePublicBaseUrl(bucket),
152
+ publicAssetPrefix: parseOptionalEnv("CMS_GENERATED_ASSET_PREFIX")
153
+ });
154
+ await service.ensureInitialized(createDefaultCmsDocument());
155
+ return service;
156
+ })();
157
+ }
158
+ return servicePromise;
159
+ }
160
+
161
+ // src/api-supabase/index.ts
162
+ var SELECTION_KIND_SET = /* @__PURE__ */ new Set([
163
+ "text",
164
+ "image",
165
+ "link",
166
+ "section"
167
+ ]);
168
+ var DEFAULT_MODEL_OPTIONS = [
169
+ {
170
+ id: "openai:gpt-5.2",
171
+ label: "OpenAI GPT-5.2",
172
+ provider: "openai"
173
+ },
174
+ {
175
+ id: "gemini:gemini-3-flash-preview",
176
+ label: "Google Gemini 3 Flash (preview)",
177
+ provider: "gemini"
178
+ },
179
+ {
180
+ id: "gemini:gemini-3.1-pro-preview",
181
+ label: "Google Gemini 3.1 Pro (preview)",
182
+ provider: "gemini"
183
+ }
184
+ ];
185
+ function buildAvailableModels(config) {
186
+ const enabledProviders = /* @__PURE__ */ new Set();
187
+ if (config.openaiEnabled) {
188
+ enabledProviders.add("openai");
189
+ }
190
+ if (config.geminiEnabled) {
191
+ enabledProviders.add("gemini");
192
+ }
193
+ const options = /* @__PURE__ */ new Map();
194
+ for (const candidate of DEFAULT_MODEL_OPTIONS) {
195
+ if (!enabledProviders.has(candidate.provider)) {
196
+ continue;
197
+ }
198
+ options.set(candidate.id, {
199
+ id: candidate.id,
200
+ label: candidate.label
201
+ });
202
+ }
203
+ return Array.from(options.values());
204
+ }
205
+ function buildModelCapabilities(input) {
206
+ const hasReadableModel = input.availableModels.length > 0;
207
+ const hasImagePipeline = input.config.geminiEnabled && input.hasPublicAssetBaseUrl;
208
+ return {
209
+ contentEdit: hasReadableModel,
210
+ themeTokenEdit: hasReadableModel,
211
+ imageGenerate: hasImagePipeline,
212
+ imageEdit: hasImagePipeline,
213
+ visionAssist: hasReadableModel
214
+ };
215
+ }
216
+ function resolveDefaultModelId(config, availableModels) {
217
+ const requestedDefault = config.defaultModelId.trim();
218
+ if (availableModels.some((model) => model.id === requestedDefault)) {
219
+ return requestedDefault;
220
+ }
221
+ return availableModels[0]?.id ?? requestedDefault;
222
+ }
223
+ function toHeaderRecord(headers) {
224
+ const out = {};
225
+ headers.forEach((value, key) => {
226
+ out[key] = value;
227
+ });
228
+ return out;
229
+ }
230
+ async function requireAdminFromHeaders(headers) {
231
+ const token = getBearerToken(toHeaderRecord(headers));
232
+ if (!token) {
233
+ throw new Error("Missing bearer token.");
234
+ }
235
+ return verifyAdminToken(token);
236
+ }
237
+ function getStageFromQuery(url) {
238
+ const stage = url.searchParams.get("stage");
239
+ if (stage === "draft") {
240
+ return "draft";
241
+ }
242
+ return "live";
243
+ }
244
+ function normalizeText(value, maxLength) {
245
+ if (typeof value !== "string") {
246
+ return null;
247
+ }
248
+ const compact = value.replace(/\s+/g, " ").trim();
249
+ if (!compact) {
250
+ return null;
251
+ }
252
+ if (compact.length <= maxLength) {
253
+ return compact;
254
+ }
255
+ return `${compact.slice(0, maxLength - 3)}...`;
256
+ }
257
+ function normalizeSelectionKind(value) {
258
+ if (typeof value !== "string") {
259
+ return null;
260
+ }
261
+ if (SELECTION_KIND_SET.has(value)) {
262
+ return value;
263
+ }
264
+ return null;
265
+ }
266
+ function normalizePagePath(value) {
267
+ if (typeof value !== "string") {
268
+ return null;
269
+ }
270
+ const [withoutQuery] = value.split(/[?#]/, 1);
271
+ const trimmed = withoutQuery?.trim() ?? "";
272
+ if (!trimmed || !trimmed.startsWith("/")) {
273
+ return null;
274
+ }
275
+ return trimmed;
276
+ }
277
+ function normalizeRelatedPaths(value) {
278
+ if (!Array.isArray(value)) {
279
+ return [];
280
+ }
281
+ const out = /* @__PURE__ */ new Set();
282
+ for (const item of value) {
283
+ const normalized = normalizeEditablePath(item);
284
+ if (normalized) {
285
+ out.add(normalized);
286
+ }
287
+ }
288
+ return Array.from(out);
289
+ }
290
+ function normalizeSelectedElementContext(value) {
291
+ if (!value || typeof value !== "object") {
292
+ return void 0;
293
+ }
294
+ const record = value;
295
+ const path = normalizeEditablePath(record.path);
296
+ const label = normalizeText(record.label, 120);
297
+ const kind = normalizeSelectionKind(record.kind);
298
+ const pagePath = normalizePagePath(record.pagePath);
299
+ if (!path || !label || !kind || !pagePath) {
300
+ return void 0;
301
+ }
302
+ const selected = {
303
+ path,
304
+ label,
305
+ kind,
306
+ pagePath
307
+ };
308
+ const relatedPaths = normalizeRelatedPaths(record.relatedPaths);
309
+ if (relatedPaths.length > 0) {
310
+ selected.relatedPaths = relatedPaths;
311
+ }
312
+ const preview = normalizeText(record.preview, 140);
313
+ if (preview) {
314
+ selected.preview = preview;
315
+ }
316
+ return selected;
317
+ }
318
+ function errorMessage(error) {
319
+ return error instanceof Error ? error.message : "Unknown error";
320
+ }
321
+ function createChatStreamResponse(input) {
322
+ const encoder = new TextEncoder();
323
+ const stream = new ReadableStream({
324
+ start(controller) {
325
+ let heartbeat = null;
326
+ let closed = false;
327
+ const close = () => {
328
+ if (closed) {
329
+ return;
330
+ }
331
+ closed = true;
332
+ if (heartbeat) {
333
+ clearInterval(heartbeat);
334
+ }
335
+ controller.close();
336
+ };
337
+ const write = (eventName, data) => {
338
+ if (closed) {
339
+ return;
340
+ }
341
+ controller.enqueue(encoder.encode(sseEvent(eventName, data)));
342
+ };
343
+ const abortHandler = () => {
344
+ close();
345
+ };
346
+ input.requestSignal.addEventListener("abort", abortHandler, { once: true });
347
+ void (async () => {
348
+ try {
349
+ const selectedElement = normalizeSelectedElementContext(input.body.selectedElement);
350
+ write("ready", { ok: true });
351
+ heartbeat = setInterval(() => {
352
+ write("ping", { ts: (/* @__PURE__ */ new Date()).toISOString() });
353
+ }, 15e3);
354
+ const result = await runAgentTurn(input.service, {
355
+ prompt: input.body.message,
356
+ modelId: input.body.modelId,
357
+ includeThinking: input.body.includeThinking,
358
+ actor: input.actor,
359
+ currentPath: input.body.currentPath,
360
+ selectedElement,
361
+ history: input.body.history,
362
+ onThinkingEvent: input.body.includeThinking ? (note) => {
363
+ write("thinking", { note });
364
+ } : void 0,
365
+ onToolEvent: (toolEvent) => {
366
+ write("tool", toolEvent);
367
+ }
368
+ });
369
+ write("message", { text: result.text });
370
+ if (result.mutationsApplied) {
371
+ write("draft-updated", {
372
+ contentVersion: result.updatedDraft.meta.contentVersion,
373
+ updatedAt: result.updatedDraft.meta.updatedAt,
374
+ summary: result.mutationSummary ?? {
375
+ contentOperations: 0,
376
+ themeTokenChanges: 0,
377
+ imageOperations: 0
378
+ }
379
+ });
380
+ }
381
+ write("done", { ok: true });
382
+ } catch (error) {
383
+ write("error", {
384
+ ok: false,
385
+ error: errorMessage(error)
386
+ });
387
+ write("done", { ok: false });
388
+ } finally {
389
+ input.requestSignal.removeEventListener("abort", abortHandler);
390
+ close();
391
+ }
392
+ })();
393
+ }
394
+ });
395
+ return new Response(stream, {
396
+ status: 200,
397
+ headers: sseHeaders()
398
+ });
399
+ }
400
+ async function handler(request) {
401
+ try {
402
+ const method = request.method.toUpperCase();
403
+ if (method === "OPTIONS") {
404
+ return jsonResponse(200, { ok: true });
405
+ }
406
+ const url = new URL(request.url);
407
+ const path = normalizePath(url.pathname);
408
+ const service = await getCmsService();
409
+ if (method === "GET" && path.endsWith("/content")) {
410
+ const stage = getStageFromQuery(url);
411
+ if (stage === "draft") {
412
+ await requireAdminFromHeaders(request.headers);
413
+ }
414
+ const content = await service.getContent(stage);
415
+ return jsonResponse(200, { stage, content });
416
+ }
417
+ if (method === "GET" && path.endsWith("/models")) {
418
+ const config = service.getModelConfig();
419
+ const availableModels = buildAvailableModels(config);
420
+ const defaultModelId = resolveDefaultModelId(config, availableModels);
421
+ const capabilities = buildModelCapabilities({
422
+ config,
423
+ availableModels,
424
+ hasPublicAssetBaseUrl: Boolean(service.getPublicAssetBaseUrl())
425
+ });
426
+ return jsonResponse(200, {
427
+ providers: {
428
+ openai: config.openaiEnabled,
429
+ gemini: config.geminiEnabled
430
+ },
431
+ capabilities,
432
+ defaultModelId,
433
+ showModelPicker: availableModels.length > 1,
434
+ availableModels
435
+ });
436
+ }
437
+ if (method === "GET" && path.endsWith("/session")) {
438
+ const identity = await requireAdminFromHeaders(request.headers);
439
+ return jsonResponse(200, {
440
+ authenticated: true,
441
+ identity
442
+ });
443
+ }
444
+ if (method === "GET" && path.endsWith("/history")) {
445
+ await requireAdminFromHeaders(request.headers);
446
+ const history = await service.listHistory();
447
+ return jsonResponse(200, history);
448
+ }
449
+ if (method === "POST" && path.endsWith("/publish")) {
450
+ const identity = await requireAdminFromHeaders(request.headers);
451
+ const body = await parseJsonBody(request);
452
+ const result = await service.publishDraft(body, identity.email ?? identity.sub);
453
+ return jsonResponse(200, {
454
+ ok: true,
455
+ version: result
456
+ });
457
+ }
458
+ if (method === "POST" && path.endsWith("/rollback")) {
459
+ const identity = await requireAdminFromHeaders(request.headers);
460
+ const body = await parseJsonBody(request);
461
+ const document = await service.rollbackDraft(body, identity.email ?? identity.sub);
462
+ return jsonResponse(200, {
463
+ ok: true,
464
+ draftVersion: document.meta.contentVersion
465
+ });
466
+ }
467
+ if (method === "POST" && path.endsWith("/checkpoints/delete")) {
468
+ await requireAdminFromHeaders(request.headers);
469
+ const body = await parseJsonBody(request);
470
+ const checkpointId = typeof body.checkpointId === "string" ? body.checkpointId.trim() : "";
471
+ if (!checkpointId) {
472
+ throw new Error("checkpointId is required.");
473
+ }
474
+ await service.deleteCheckpoint(checkpointId);
475
+ return jsonResponse(200, {
476
+ ok: true,
477
+ checkpointId
478
+ });
479
+ }
480
+ if (method === "POST" && path.endsWith("/chat/stream")) {
481
+ const identity = await requireAdminFromHeaders(request.headers);
482
+ const body = await parseJsonBody(request);
483
+ return createChatStreamResponse({
484
+ service,
485
+ requestSignal: request.signal,
486
+ body,
487
+ actor: identity.email ?? identity.sub
488
+ });
489
+ }
490
+ return jsonResponse(404, {
491
+ error: "Not found",
492
+ path,
493
+ method
494
+ });
495
+ } catch (error) {
496
+ const message = errorMessage(error);
497
+ const statusCode = message.startsWith("Checkpoint not found:") ? 404 : 400;
498
+ return jsonResponse(statusCode, {
499
+ ok: false,
500
+ error: message
501
+ });
502
+ }
503
+ }
504
+ var api_supabase_default = handler;
505
+
506
+ export {
507
+ handler,
508
+ api_supabase_default
509
+ };