@the-portland-company/devnotes 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,1187 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server/deno.ts
21
+ var deno_exports = {};
22
+ __export(deno_exports, {
23
+ createDenoDevNotesHandler: () => createDenoDevNotesHandler
24
+ });
25
+ module.exports = __toCommonJS(deno_exports);
26
+
27
+ // src/server/forge.ts
28
+ var DEFAULT_BASE_PATH = "/api/devnotes";
29
+ var DEVNOTES_META_MARKER = "[DEVNOTES_META:";
30
+ var DEVNOTES_DEFAULT_TYPE_NAMES = ["Bug", "Feature Request", "UI Issue", "Performance"];
31
+ var DEVNOTES_DEFAULT_TASK_LIST_NAME = "General";
32
+ var UpstreamForgeError = class extends Error {
33
+ constructor(path, baseUrl, response) {
34
+ super(`Focus Forge request failed for ${path}`);
35
+ this.name = "UpstreamForgeError";
36
+ this.path = path;
37
+ this.baseUrl = baseUrl;
38
+ this.response = response;
39
+ }
40
+ };
41
+ function coerceObject(value) {
42
+ if (value && typeof value === "object" && !Array.isArray(value)) {
43
+ return value;
44
+ }
45
+ return {};
46
+ }
47
+ function parseJsonSafe(text) {
48
+ if (!text) return null;
49
+ try {
50
+ return JSON.parse(text);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function bytesToBase64(bytes) {
56
+ if (typeof Buffer !== "undefined") {
57
+ return Buffer.from(bytes).toString("base64");
58
+ }
59
+ let binary = "";
60
+ for (let i = 0; i < bytes.length; i += 1) {
61
+ binary += String.fromCharCode(bytes[i]);
62
+ }
63
+ return btoa(binary);
64
+ }
65
+ function base64ToBytes(value) {
66
+ if (typeof Buffer !== "undefined") {
67
+ return new Uint8Array(Buffer.from(value, "base64"));
68
+ }
69
+ const binary = atob(value);
70
+ const bytes = new Uint8Array(binary.length);
71
+ for (let i = 0; i < binary.length; i += 1) {
72
+ bytes[i] = binary.charCodeAt(i);
73
+ }
74
+ return bytes;
75
+ }
76
+ function normalizeForgeBoolean(value, fallback = false) {
77
+ if (typeof value === "boolean") return value;
78
+ if (typeof value === "string") {
79
+ const normalized = value.trim().toLowerCase();
80
+ if (normalized === "true") return true;
81
+ if (normalized === "false") return false;
82
+ }
83
+ return fallback;
84
+ }
85
+ function normalizeForgeNumber(value, fallback = 0) {
86
+ const parsed = Number(value);
87
+ return Number.isFinite(parsed) ? parsed : fallback;
88
+ }
89
+ function normalizeForgeStringArray(value) {
90
+ if (!Array.isArray(value)) return [];
91
+ return value.map((item) => String(item || "").trim()).filter(Boolean);
92
+ }
93
+ function mapBugStatusToForge(status) {
94
+ const normalized = status.trim().toLowerCase();
95
+ if (normalized === "in progress") return "in_progress";
96
+ if (normalized === "resolved" || normalized === "closed") return "completed";
97
+ return "open";
98
+ }
99
+ function encodeDevNotesMeta(meta) {
100
+ return bytesToBase64(new TextEncoder().encode(JSON.stringify(meta)));
101
+ }
102
+ function decodeDevNotesMeta(encoded) {
103
+ try {
104
+ const text = new TextDecoder().decode(base64ToBytes(encoded));
105
+ const parsed = JSON.parse(text);
106
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ function splitDevNotesMeta(text) {
112
+ const value = String(text || "");
113
+ const markerIndex = value.lastIndexOf(DEVNOTES_META_MARKER);
114
+ if (markerIndex === -1) {
115
+ return { body: value, meta: null };
116
+ }
117
+ const endIndex = value.indexOf("]", markerIndex);
118
+ if (endIndex === -1) {
119
+ return { body: value, meta: null };
120
+ }
121
+ const encoded = value.slice(markerIndex + DEVNOTES_META_MARKER.length, endIndex).trim();
122
+ const meta = decodeDevNotesMeta(encoded);
123
+ if (!meta) {
124
+ return { body: value, meta: null };
125
+ }
126
+ const body = value.slice(0, markerIndex).replace(/\n+$/, "");
127
+ return { body, meta };
128
+ }
129
+ function appendDevNotesMeta(text, meta) {
130
+ const body = String(text || "").trimEnd();
131
+ const marker = `${DEVNOTES_META_MARKER}${encodeDevNotesMeta(meta)}]`;
132
+ return body ? `${body}
133
+
134
+ ${marker}` : marker;
135
+ }
136
+ function parseLegacyDevNotesDescription(description) {
137
+ const legacyMarker = "\n\n---\nSource: Politogy bug report";
138
+ const index = description.indexOf(legacyMarker);
139
+ if (index === -1) {
140
+ return {
141
+ description: description.trim() || null
142
+ };
143
+ }
144
+ const leading = description.slice(0, index).trim() || null;
145
+ const detailLines = description.slice(index + "\n\n---\n".length).split("\n").map((line) => line.trim()).filter(Boolean);
146
+ const values = /* @__PURE__ */ new Map();
147
+ detailLines.forEach((line) => {
148
+ const separator = line.indexOf(":");
149
+ if (separator === -1) return;
150
+ const key = line.slice(0, separator).trim().toLowerCase();
151
+ const value = line.slice(separator + 1).trim();
152
+ if (key) values.set(key, value);
153
+ });
154
+ return {
155
+ description: leading,
156
+ page_url: values.get("page url") || "",
157
+ x_position: normalizeForgeNumber(values.get("coordinates")?.split(",")[0]?.trim(), 0),
158
+ y_position: normalizeForgeNumber(values.get("coordinates")?.split(",")[1]?.trim(), 0),
159
+ severity: values.get("severity") || "Medium",
160
+ status: values.get("status") || "Open",
161
+ types: values.get("types")?.split(",").map((item) => item.trim()).filter(Boolean) || [],
162
+ creator_name: values.get("created by") || ""
163
+ };
164
+ }
165
+ function buildDevNotesReportDescription(report) {
166
+ const description = typeof report.description === "string" ? report.description : "";
167
+ return appendDevNotesMeta(description, {
168
+ kind: "report",
169
+ version: 1,
170
+ task_list_id: String(report.task_list_id || ""),
171
+ page_url: String(report.page_url || ""),
172
+ x_position: normalizeForgeNumber(report.x_position),
173
+ y_position: normalizeForgeNumber(report.y_position),
174
+ target_selector: report.target_selector === null || report.target_selector === void 0 ? null : String(report.target_selector),
175
+ target_relative_x: report.target_relative_x === null || report.target_relative_x === void 0 ? null : normalizeForgeNumber(report.target_relative_x),
176
+ target_relative_y: report.target_relative_y === null || report.target_relative_y === void 0 ? null : normalizeForgeNumber(report.target_relative_y),
177
+ types: normalizeForgeStringArray(report.types),
178
+ severity: String(report.severity || "Medium"),
179
+ expected_behavior: report.expected_behavior === null || report.expected_behavior === void 0 ? null : String(report.expected_behavior),
180
+ actual_behavior: report.actual_behavior === null || report.actual_behavior === void 0 ? null : String(report.actual_behavior),
181
+ capture_context: report.capture_context && typeof report.capture_context === "object" ? report.capture_context : null,
182
+ status: String(report.status || "Open"),
183
+ created_by: String(report.created_by || ""),
184
+ creator_name: report.creator && typeof report.creator === "object" ? String(report.creator.full_name || "") : "",
185
+ creator_email: report.creator && typeof report.creator === "object" ? String(report.creator.email || "") : "",
186
+ assigned_to: report.assigned_to === null || report.assigned_to === void 0 ? null : String(report.assigned_to),
187
+ resolved_at: report.resolved_at === null || report.resolved_at === void 0 ? null : String(report.resolved_at),
188
+ resolved_by: report.resolved_by === null || report.resolved_by === void 0 ? null : String(report.resolved_by),
189
+ approved: normalizeForgeBoolean(report.approved),
190
+ ai_ready: normalizeForgeBoolean(report.ai_ready),
191
+ ai_description: report.ai_description === null || report.ai_description === void 0 ? null : String(report.ai_description),
192
+ response: report.response === null || report.response === void 0 ? null : String(report.response)
193
+ });
194
+ }
195
+ function isDevNotesForgeTask(task) {
196
+ const description = String(task.description || "");
197
+ const parsed = splitDevNotesMeta(description);
198
+ if (parsed.meta?.kind === "report") return true;
199
+ return description.includes("Source: Politogy bug report");
200
+ }
201
+ function extractProjectsFromPayload(payload) {
202
+ const root = coerceObject(payload);
203
+ const data = coerceObject(root.data);
204
+ const bootstrap = coerceObject(data.bootstrap);
205
+ const projectArrays = [
206
+ Array.isArray(root.projects) ? root.projects : [],
207
+ Array.isArray(data.projects) ? data.projects : [],
208
+ Array.isArray(bootstrap.projects) ? bootstrap.projects : []
209
+ ];
210
+ const seen = /* @__PURE__ */ new Set();
211
+ const projects = [];
212
+ for (const group of projectArrays) {
213
+ for (const projectRaw of group) {
214
+ const project = coerceObject(projectRaw);
215
+ const id = typeof project.id === "string" ? project.id.trim() : "";
216
+ const name = typeof project.name === "string" ? project.name.trim() : "";
217
+ if (!id || !name || seen.has(id)) continue;
218
+ seen.add(id);
219
+ const organizationId = typeof project.organization_id === "string" && project.organization_id.trim() || typeof project.organizationId === "string" && project.organizationId.trim() || void 0;
220
+ projects.push({ id, name, organizationId });
221
+ }
222
+ }
223
+ return projects;
224
+ }
225
+ function extractForgeTaskId(payload) {
226
+ const root = coerceObject(payload);
227
+ const data = coerceObject(root.data);
228
+ const task = coerceObject(root.task);
229
+ const dataTask = coerceObject(data.task);
230
+ const candidates = [root.id, task.id, data.id, dataTask.id];
231
+ const invalidSentinelValues = /* @__PURE__ */ new Set(["NOT_FOUND", "not_found", "null", "undefined", ""]);
232
+ for (const candidate of candidates) {
233
+ if (typeof candidate === "string" && candidate.trim()) {
234
+ const value = candidate.trim();
235
+ if (invalidSentinelValues.has(value)) continue;
236
+ if (value.toLowerCase().startsWith("error-")) continue;
237
+ return value;
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function generateDevNotesShareSlug() {
243
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 24);
244
+ }
245
+ function normalizeBasePath(basePath) {
246
+ const trimmed = (basePath || DEFAULT_BASE_PATH).trim();
247
+ if (!trimmed) return DEFAULT_BASE_PATH;
248
+ const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
249
+ return normalized.endsWith("/") && normalized !== "/" ? normalized.slice(0, -1) : normalized;
250
+ }
251
+ function normalizeBaseUrl(baseUrl) {
252
+ return baseUrl.trim().replace(/\/+$/, "");
253
+ }
254
+ function normalizeUser(user) {
255
+ if (!user?.id) return null;
256
+ return {
257
+ id: String(user.id).trim(),
258
+ email: user.email == null ? null : String(user.email).trim(),
259
+ fullName: user.fullName == null ? null : String(user.fullName).trim(),
260
+ role: user.role == null ? null : String(user.role).trim()
261
+ };
262
+ }
263
+ function toCreatorRecord(user) {
264
+ return {
265
+ id: user.id,
266
+ email: user.email == null ? null : user.email,
267
+ full_name: user.fullName == null ? null : user.fullName
268
+ };
269
+ }
270
+ async function resolveCorsHeaders(request, corsHeaders) {
271
+ const headers = new Headers();
272
+ if (!corsHeaders) return headers;
273
+ const resolved = typeof corsHeaders === "function" ? await corsHeaders(request) : corsHeaders;
274
+ new Headers(resolved).forEach((value, key) => headers.set(key, value));
275
+ return headers;
276
+ }
277
+ async function jsonResponse(request, corsHeaders, body, status = 200) {
278
+ const headers = await resolveCorsHeaders(request, corsHeaders);
279
+ headers.set("Content-Type", "application/json");
280
+ return new Response(JSON.stringify(body), { status, headers });
281
+ }
282
+ async function emptyResponse(request, corsHeaders, status = 204) {
283
+ const headers = await resolveCorsHeaders(request, corsHeaders);
284
+ return new Response(null, { status, headers });
285
+ }
286
+ async function passthroughUpstreamResponse(request, corsHeaders, error) {
287
+ const headers = await resolveCorsHeaders(request, corsHeaders);
288
+ if (error.response.contentType) {
289
+ headers.set("Content-Type", error.response.contentType);
290
+ }
291
+ if (error.response.text) {
292
+ headers.set("X-DevNotes-Upstream-Path", error.path);
293
+ headers.set("X-DevNotes-Upstream-Base-Url", error.baseUrl);
294
+ return new Response(error.response.text, {
295
+ status: error.response.status,
296
+ headers
297
+ });
298
+ }
299
+ headers.set("Content-Type", "application/json");
300
+ return new Response(
301
+ JSON.stringify({
302
+ error: "Focus Forge request failed",
303
+ path: error.path,
304
+ baseUrl: error.baseUrl,
305
+ status: error.response.status
306
+ }),
307
+ {
308
+ status: error.response.status,
309
+ headers
310
+ }
311
+ );
312
+ }
313
+ async function readJsonBody(request) {
314
+ if (request.method === "GET" || request.method === "HEAD") return null;
315
+ const text = await request.text();
316
+ if (!text) return null;
317
+ try {
318
+ return JSON.parse(text);
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+ function parseRequestPath(pathname, basePath) {
324
+ if (pathname === basePath) return [];
325
+ if (!pathname.startsWith(`${basePath}/`)) return null;
326
+ return pathname.slice(basePath.length + 1).split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
327
+ }
328
+ function toProjectDiscovery(discovery) {
329
+ return {
330
+ path: discovery.discoveryPath,
331
+ baseUrl: discovery.resolvedBaseUrl
332
+ };
333
+ }
334
+ function buildKnownUsers(metadataComments, reports, currentUser) {
335
+ const users = /* @__PURE__ */ new Map();
336
+ users.set(currentUser.id, {
337
+ id: currentUser.id,
338
+ email: currentUser.email ?? null,
339
+ full_name: currentUser.fullName ?? null
340
+ });
341
+ reports.forEach((report) => {
342
+ users.set(report.created_by, report.creator || {
343
+ id: report.created_by,
344
+ email: null,
345
+ full_name: null
346
+ });
347
+ if (report.assigned_to && !users.has(report.assigned_to)) {
348
+ users.set(report.assigned_to, { id: report.assigned_to, email: null, full_name: null });
349
+ }
350
+ if (report.resolved_by && !users.has(report.resolved_by)) {
351
+ users.set(report.resolved_by, { id: report.resolved_by, email: null, full_name: null });
352
+ }
353
+ });
354
+ metadataComments.forEach((comment) => {
355
+ if (comment.meta.kind !== "message") return;
356
+ const authorId = String(comment.meta.authorId || comment.user_id || "").trim();
357
+ if (!authorId) return;
358
+ users.set(authorId, {
359
+ id: authorId,
360
+ email: comment.meta.authorEmail === null || comment.meta.authorEmail === void 0 ? null : String(comment.meta.authorEmail),
361
+ full_name: comment.meta.authorName === null || comment.meta.authorName === void 0 ? null : String(comment.meta.authorName)
362
+ });
363
+ });
364
+ return users;
365
+ }
366
+ async function maybeResolveUsers(resolveUsers, ids) {
367
+ if (!resolveUsers || ids.length === 0) return [];
368
+ const resolved = await resolveUsers(ids);
369
+ return resolved.filter((user) => Boolean(user?.id)).map((user) => toCreatorRecord({
370
+ id: String(user.id).trim(),
371
+ email: user.email ?? null,
372
+ fullName: user.fullName ?? null
373
+ }));
374
+ }
375
+ function pickTaskArray(payload) {
376
+ const data = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
377
+ return data.map((item) => coerceObject(item));
378
+ }
379
+ function parseDevNotesProjectComment(comment) {
380
+ const id = typeof comment.id === "string" ? comment.id.trim() : "";
381
+ if (!id) return null;
382
+ const parsed = splitDevNotesMeta(String(comment.content || ""));
383
+ const kind = typeof parsed.meta?.kind === "string" ? parsed.meta.kind : "";
384
+ if (!kind) return null;
385
+ return {
386
+ id,
387
+ created_at: String(comment.created_at || (/* @__PURE__ */ new Date()).toISOString()),
388
+ updated_at: String(comment.updated_at || comment.created_at || (/* @__PURE__ */ new Date()).toISOString()),
389
+ user_id: comment.user_id === null || comment.user_id === void 0 ? null : String(comment.user_id),
390
+ author_name: comment.author_name === null || comment.author_name === void 0 ? null : String(comment.author_name),
391
+ author_email: comment.author_email === null || comment.author_email === void 0 ? null : String(comment.author_email),
392
+ body: parsed.body,
393
+ meta: parsed.meta || {}
394
+ };
395
+ }
396
+ function buildTaskListsFromMetadata(comments) {
397
+ return comments.filter((item) => item.meta.kind === "task_list").sort((a, b) => a.created_at.localeCompare(b.created_at)).map((item) => ({
398
+ id: item.id,
399
+ name: String(item.meta.name || DEVNOTES_DEFAULT_TASK_LIST_NAME),
400
+ share_slug: String(item.meta.share_slug || generateDevNotesShareSlug()),
401
+ is_default: normalizeForgeBoolean(item.meta.is_default),
402
+ created_by: item.meta.created_by === null || item.meta.created_by === void 0 ? null : String(item.meta.created_by),
403
+ created_at: String(item.meta.created_at || item.created_at),
404
+ updated_at: String(item.meta.updated_at || item.updated_at)
405
+ }));
406
+ }
407
+ function buildReportTypesFromMetadata(comments) {
408
+ return comments.filter((item) => item.meta.kind === "report_type").sort((a, b) => String(a.meta.name || "").localeCompare(String(b.meta.name || ""))).map((item) => ({
409
+ id: item.id,
410
+ name: String(item.meta.name || ""),
411
+ is_default: normalizeForgeBoolean(item.meta.is_default),
412
+ created_by: item.meta.created_by === null || item.meta.created_by === void 0 ? null : String(item.meta.created_by),
413
+ created_at: String(item.meta.created_at || item.created_at)
414
+ }));
415
+ }
416
+ function buildDevNotesReportFromForgeTask(task, overrides, defaultTaskListId) {
417
+ if (!isDevNotesForgeTask(task)) return null;
418
+ const taskId = typeof task.id === "string" ? task.id.trim() : "";
419
+ if (!taskId) return null;
420
+ const parsed = splitDevNotesMeta(String(task.description || ""));
421
+ const base = parsed.meta?.kind === "report" ? parsed.meta : parseLegacyDevNotesDescription(String(task.description || ""));
422
+ const combined = {
423
+ ...base,
424
+ ...overrides || {}
425
+ };
426
+ const creatorName = String(combined.creator_name || combined.created_by || "").trim();
427
+ const creatorEmail = String(combined.creator_email || "").trim() || null;
428
+ const createdBy = String(combined.created_by || "").trim() || `forge:${taskId}:creator`;
429
+ const taskCompleted = normalizeForgeBoolean(task.completed);
430
+ const status = String(combined.status || (taskCompleted ? "Resolved" : "Open"));
431
+ const description = overrides && Object.prototype.hasOwnProperty.call(overrides, "description") ? overrides.description === null ? null : String(overrides.description || "") : parsed.body.trim() || (base.description ? String(base.description) : null);
432
+ return {
433
+ id: taskId,
434
+ task_list_id: String(combined.task_list_id || defaultTaskListId || ""),
435
+ page_url: String(combined.page_url || ""),
436
+ x_position: normalizeForgeNumber(combined.x_position),
437
+ y_position: normalizeForgeNumber(combined.y_position),
438
+ target_selector: combined.target_selector === null || combined.target_selector === void 0 ? null : String(combined.target_selector),
439
+ target_relative_x: combined.target_relative_x === null || combined.target_relative_x === void 0 ? null : normalizeForgeNumber(combined.target_relative_x),
440
+ target_relative_y: combined.target_relative_y === null || combined.target_relative_y === void 0 ? null : normalizeForgeNumber(combined.target_relative_y),
441
+ types: normalizeForgeStringArray(combined.types),
442
+ severity: String(combined.severity || "Medium"),
443
+ title: String(overrides?.title || task.name || ""),
444
+ description,
445
+ expected_behavior: combined.expected_behavior === null || combined.expected_behavior === void 0 ? null : String(combined.expected_behavior),
446
+ actual_behavior: combined.actual_behavior === null || combined.actual_behavior === void 0 ? null : String(combined.actual_behavior),
447
+ capture_context: combined.capture_context && typeof combined.capture_context === "object" ? combined.capture_context : null,
448
+ response: combined.response === null || combined.response === void 0 ? null : String(combined.response),
449
+ status,
450
+ created_by: createdBy,
451
+ creator: {
452
+ id: createdBy,
453
+ email: creatorEmail,
454
+ full_name: creatorName || null
455
+ },
456
+ assigned_to: overrides && Object.prototype.hasOwnProperty.call(overrides, "assigned_to") ? overrides.assigned_to === null || overrides.assigned_to === void 0 ? null : String(overrides.assigned_to) : task.assigned_to === null || task.assigned_to === void 0 ? null : String(task.assigned_to),
457
+ resolved_at: combined.resolved_at === null || combined.resolved_at === void 0 ? taskCompleted ? String(task.completed_at || task.updated_at || task.created_at || "") : null : String(combined.resolved_at),
458
+ resolved_by: combined.resolved_by === null || combined.resolved_by === void 0 ? null : String(combined.resolved_by),
459
+ approved: normalizeForgeBoolean(combined.approved),
460
+ ai_ready: normalizeForgeBoolean(combined.ai_ready),
461
+ ai_description: combined.ai_description === null || combined.ai_description === void 0 ? null : String(combined.ai_description),
462
+ created_at: String(task.created_at || (/* @__PURE__ */ new Date()).toISOString()),
463
+ updated_at: String(
464
+ overrides?.updated_at || task.updated_at || task.created_at || (/* @__PURE__ */ new Date()).toISOString()
465
+ )
466
+ };
467
+ }
468
+ async function fetchFocusForge(context, path, init = {}) {
469
+ const url = `${context.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
470
+ const headers = new Headers(init.headers || {});
471
+ headers.set("Authorization", `Bearer ${context.pat}`);
472
+ if (!headers.has("Content-Type") && init.body) {
473
+ headers.set("Content-Type", "application/json");
474
+ }
475
+ const response = await context.fetchImpl(url, {
476
+ ...init,
477
+ headers
478
+ });
479
+ const text = await response.text();
480
+ return {
481
+ status: response.status,
482
+ ok: response.ok,
483
+ text,
484
+ payload: parseJsonSafe(text),
485
+ contentType: response.headers.get("content-type")
486
+ };
487
+ }
488
+ async function fetchForgeOrThrow(context, path, init = {}) {
489
+ const response = await fetchFocusForge(context, path, init);
490
+ if (!response.ok) {
491
+ throw new UpstreamForgeError(path, context.baseUrl, response);
492
+ }
493
+ return response;
494
+ }
495
+ async function discoverForgeProjects(context) {
496
+ const bootstrap = await fetchFocusForge(context, "/api/mobile/bootstrap", { method: "GET" });
497
+ if (!bootstrap.ok) {
498
+ return {
499
+ ok: false,
500
+ preferredProjectName: context.projectName,
501
+ resolvedBaseUrl: context.baseUrl,
502
+ discoveryPath: "/api/mobile/bootstrap",
503
+ response: bootstrap
504
+ };
505
+ }
506
+ const bootstrapProjects = extractProjectsFromPayload(bootstrap.payload);
507
+ if (bootstrapProjects.length > 0) {
508
+ return {
509
+ ok: true,
510
+ project: null,
511
+ matched: false,
512
+ preferredProjectName: context.projectName,
513
+ projects: bootstrapProjects,
514
+ resolvedBaseUrl: context.baseUrl,
515
+ discoveryPath: "/api/mobile/bootstrap"
516
+ };
517
+ }
518
+ const projectsResponse = await fetchFocusForge(context, "/api/mobile/projects", { method: "GET" });
519
+ if (!projectsResponse.ok) {
520
+ return {
521
+ ok: false,
522
+ preferredProjectName: context.projectName,
523
+ resolvedBaseUrl: context.baseUrl,
524
+ discoveryPath: "/api/mobile/projects",
525
+ response: projectsResponse
526
+ };
527
+ }
528
+ return {
529
+ ok: true,
530
+ project: null,
531
+ matched: false,
532
+ preferredProjectName: context.projectName,
533
+ projects: extractProjectsFromPayload(projectsResponse.payload),
534
+ resolvedBaseUrl: context.baseUrl,
535
+ discoveryPath: "/api/mobile/projects"
536
+ };
537
+ }
538
+ async function resolveForgeProject(context) {
539
+ const discovery = await discoverForgeProjects(context);
540
+ if (!discovery.ok) return discovery;
541
+ if (!context.projectName) {
542
+ return {
543
+ ...discovery,
544
+ preferredProjectName: null,
545
+ matched: false,
546
+ project: null
547
+ };
548
+ }
549
+ const matchedProject = discovery.projects.find(
550
+ (project) => project.name.trim().toLowerCase() === context.projectName.trim().toLowerCase()
551
+ );
552
+ return {
553
+ ...discovery,
554
+ preferredProjectName: context.projectName,
555
+ matched: Boolean(matchedProject),
556
+ project: matchedProject || null
557
+ };
558
+ }
559
+ async function fetchForgeTasksForProject(context, projectId) {
560
+ const response = await fetchForgeOrThrow(
561
+ context,
562
+ `/api/mobile/tasks?projectId=${encodeURIComponent(projectId)}`,
563
+ { method: "GET" }
564
+ );
565
+ return pickTaskArray(response.payload);
566
+ }
567
+ async function fetchForgeProjectComments(context, projectId) {
568
+ const response = await fetchForgeOrThrow(
569
+ context,
570
+ `/api/sync/comments?projectId=${encodeURIComponent(projectId)}`,
571
+ { method: "GET" }
572
+ );
573
+ return pickTaskArray(response.payload);
574
+ }
575
+ async function fetchForgeTaskComments(context, taskId) {
576
+ const response = await fetchForgeOrThrow(
577
+ context,
578
+ `/api/sync/comments?taskId=${encodeURIComponent(taskId)}`,
579
+ { method: "GET" }
580
+ );
581
+ return pickTaskArray(response.payload);
582
+ }
583
+ async function fetchForgeCommentById(context, commentId) {
584
+ const response = await fetchForgeOrThrow(
585
+ context,
586
+ `/api/sync/comments/${encodeURIComponent(commentId)}`,
587
+ { method: "GET" }
588
+ );
589
+ const payload = coerceObject(response.payload?.data || response.payload);
590
+ return Object.keys(payload).length > 0 ? payload : null;
591
+ }
592
+ async function createForgeProjectComment(context, projectId, body, meta) {
593
+ const response = await fetchForgeOrThrow(context, "/api/sync/comments", {
594
+ method: "POST",
595
+ body: JSON.stringify({
596
+ projectId,
597
+ content: appendDevNotesMeta(body, meta)
598
+ })
599
+ });
600
+ return coerceObject(response.payload?.data || response.payload);
601
+ }
602
+ async function updateForgeProjectComment(context, commentId, body, meta) {
603
+ const response = await fetchForgeOrThrow(
604
+ context,
605
+ `/api/sync/comments/${encodeURIComponent(commentId)}`,
606
+ {
607
+ method: "PUT",
608
+ body: JSON.stringify({
609
+ content: appendDevNotesMeta(body, meta)
610
+ })
611
+ }
612
+ );
613
+ return coerceObject(response.payload?.data || response.payload);
614
+ }
615
+ async function deleteForgeProjectComment(context, commentId) {
616
+ await fetchForgeOrThrow(context, `/api/sync/comments/${encodeURIComponent(commentId)}`, {
617
+ method: "DELETE"
618
+ });
619
+ }
620
+ async function ensureDevNotesProjectDefaults(context, projectId, user) {
621
+ let parsed = (await fetchForgeProjectComments(context, projectId)).map(parseDevNotesProjectComment).filter((item) => Boolean(item));
622
+ const typeComments = parsed.filter((item) => item.meta.kind === "report_type");
623
+ const taskListComments = parsed.filter((item) => item.meta.kind === "task_list");
624
+ if (typeComments.length === 0) {
625
+ for (const name of DEVNOTES_DEFAULT_TYPE_NAMES) {
626
+ await createForgeProjectComment(context, projectId, "", {
627
+ kind: "report_type",
628
+ name,
629
+ is_default: true,
630
+ created_by: user.id,
631
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
632
+ });
633
+ }
634
+ }
635
+ if (taskListComments.length === 0) {
636
+ await createForgeProjectComment(context, projectId, "", {
637
+ kind: "task_list",
638
+ name: DEVNOTES_DEFAULT_TASK_LIST_NAME,
639
+ share_slug: generateDevNotesShareSlug(),
640
+ is_default: true,
641
+ created_by: user.id,
642
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
643
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
644
+ });
645
+ }
646
+ if (typeComments.length === 0 || taskListComments.length === 0) {
647
+ parsed = (await fetchForgeProjectComments(context, projectId)).map(parseDevNotesProjectComment).filter((item) => Boolean(item));
648
+ }
649
+ return parsed;
650
+ }
651
+ function buildCapabilities() {
652
+ return { ai: false, appLink: true };
653
+ }
654
+ function buildAppLinkStatus(context, discovery) {
655
+ if (!context.pat) {
656
+ return {
657
+ linked: false,
658
+ projectName: context.projectName,
659
+ tokenLast4: null,
660
+ linkedAt: null,
661
+ projectMatched: false,
662
+ availableProjects: [],
663
+ projectDiscovery: null
664
+ };
665
+ }
666
+ if (!discovery.ok) {
667
+ return {
668
+ linked: true,
669
+ projectName: context.projectName,
670
+ tokenLast4: context.pat.slice(-4),
671
+ linkedAt: null,
672
+ projectMatched: false,
673
+ availableProjects: [],
674
+ projectDiscovery: toProjectDiscovery(discovery)
675
+ };
676
+ }
677
+ return {
678
+ linked: true,
679
+ projectName: context.projectName,
680
+ tokenLast4: context.pat.slice(-4),
681
+ linkedAt: null,
682
+ projectMatched: discovery.matched,
683
+ availableProjects: discovery.matched ? [] : discovery.projects,
684
+ projectDiscovery: toProjectDiscovery(discovery)
685
+ };
686
+ }
687
+ function sortCreators(creators) {
688
+ return creators.sort((left, right) => {
689
+ const a = left.full_name || left.email || left.id;
690
+ const b = right.full_name || right.email || right.id;
691
+ return a.localeCompare(b);
692
+ });
693
+ }
694
+ function createDevNotesServerHandler(options) {
695
+ const basePath = normalizeBasePath(options.basePath);
696
+ const baseUrl = normalizeBaseUrl(options.forge.baseUrl);
697
+ if (!baseUrl) {
698
+ throw new Error("DevNotes server helpers require forge.baseUrl.");
699
+ }
700
+ const fetchImpl = options.fetch ?? globalThis.fetch;
701
+ if (typeof fetchImpl !== "function") {
702
+ throw new Error("DevNotes server helpers require a fetch implementation.");
703
+ }
704
+ return async function handleDevNotesRequest(request) {
705
+ if (request.method === "OPTIONS") {
706
+ return await emptyResponse(request, options.corsHeaders);
707
+ }
708
+ const url = new URL(request.url);
709
+ const slug = parseRequestPath(url.pathname, basePath);
710
+ if (!slug) {
711
+ return await jsonResponse(request, options.corsHeaders, { error: "Not found" }, 404);
712
+ }
713
+ const user = normalizeUser(await options.getCurrentUser(request));
714
+ if (!user) {
715
+ return await jsonResponse(request, options.corsHeaders, { error: "Unauthorized" }, 401);
716
+ }
717
+ const method = request.method.toUpperCase();
718
+ const body = await readJsonBody(request) || {};
719
+ const [resource, resourceId, nested] = slug;
720
+ const forgeContext = {
721
+ baseUrl,
722
+ pat: String(options.forge.pat || "").trim(),
723
+ projectName: options.forge.projectName?.trim() || null,
724
+ fetchImpl
725
+ };
726
+ if (resource === "capabilities" && method === "GET") {
727
+ return await jsonResponse(request, options.corsHeaders, buildCapabilities());
728
+ }
729
+ if (!forgeContext.pat && resource === "app-link" && method === "GET") {
730
+ return await jsonResponse(
731
+ request,
732
+ options.corsHeaders,
733
+ buildAppLinkStatus(forgeContext, {
734
+ ok: true,
735
+ project: null,
736
+ matched: false,
737
+ preferredProjectName: forgeContext.projectName,
738
+ projects: [],
739
+ resolvedBaseUrl: forgeContext.baseUrl,
740
+ discoveryPath: null
741
+ })
742
+ );
743
+ }
744
+ if (!forgeContext.pat) {
745
+ return await jsonResponse(
746
+ request,
747
+ options.corsHeaders,
748
+ {
749
+ error: "FOCUS_FORGE_PAT is not configured."
750
+ },
751
+ 503
752
+ );
753
+ }
754
+ try {
755
+ const projectResolution = await resolveForgeProject(forgeContext);
756
+ if (resource === "app-link") {
757
+ if (method === "GET") {
758
+ return await jsonResponse(
759
+ request,
760
+ options.corsHeaders,
761
+ buildAppLinkStatus(forgeContext, projectResolution)
762
+ );
763
+ }
764
+ return await jsonResponse(
765
+ request,
766
+ options.corsHeaders,
767
+ {
768
+ error: "App-level Forge credentials are managed through server environment configuration."
769
+ },
770
+ 405
771
+ );
772
+ }
773
+ if (!projectResolution.ok) {
774
+ throw new UpstreamForgeError(
775
+ projectResolution.discoveryPath || "/api/mobile/bootstrap",
776
+ projectResolution.resolvedBaseUrl,
777
+ projectResolution.response
778
+ );
779
+ }
780
+ if (!projectResolution.project?.id) {
781
+ return await jsonResponse(
782
+ request,
783
+ options.corsHeaders,
784
+ {
785
+ error: projectResolution.preferredProjectName ? `Could not find Focus Forge project "${projectResolution.preferredProjectName}"` : "FOCUS_FORGE_PROJECT_NAME is not configured",
786
+ available_projects: projectResolution.projects
787
+ },
788
+ projectResolution.preferredProjectName ? 404 : 409
789
+ );
790
+ }
791
+ const project = projectResolution.project;
792
+ const metadataComments = await ensureDevNotesProjectDefaults(forgeContext, project.id, user);
793
+ const taskLists = buildTaskListsFromMetadata(metadataComments);
794
+ const defaultTaskListId = String(
795
+ taskLists.find((item) => item.is_default)?.id || taskLists[0]?.id || ""
796
+ );
797
+ const reportPatchById = /* @__PURE__ */ new Map();
798
+ const deletedReportIds = /* @__PURE__ */ new Set();
799
+ const readMarkers = /* @__PURE__ */ new Set();
800
+ metadataComments.forEach((comment) => {
801
+ const kind = String(comment.meta.kind || "");
802
+ const reportId = String(comment.meta.reportId || "").trim();
803
+ if (kind === "report_patch" && reportId) {
804
+ const previous = reportPatchById.get(reportId);
805
+ if (!previous || String(previous.updated_at || "") <= comment.updated_at) {
806
+ reportPatchById.set(reportId, {
807
+ ...comment.meta.report && typeof comment.meta.report === "object" ? comment.meta.report : {},
808
+ updated_at: comment.updated_at
809
+ });
810
+ }
811
+ }
812
+ if (kind === "report_deleted" && reportId) {
813
+ deletedReportIds.add(reportId);
814
+ }
815
+ if (kind === "message_read") {
816
+ const targetMessageId = String(comment.meta.messageId || "").trim();
817
+ const targetUserId = String(comment.meta.userId || "").trim();
818
+ if (targetMessageId && targetUserId === user.id) {
819
+ readMarkers.add(targetMessageId);
820
+ }
821
+ }
822
+ });
823
+ if (resource === "reports" && method === "GET" && !resourceId) {
824
+ const tasks = await fetchForgeTasksForProject(forgeContext, project.id);
825
+ const reports = tasks.map(
826
+ (task) => buildDevNotesReportFromForgeTask(
827
+ task,
828
+ reportPatchById.get(String(task.id || "").trim()) || null,
829
+ defaultTaskListId
830
+ )
831
+ ).filter((item) => Boolean(item)).filter((item) => !deletedReportIds.has(String(item.id)));
832
+ return await jsonResponse(request, options.corsHeaders, reports);
833
+ }
834
+ if (resource === "reports" && method === "POST" && !resourceId) {
835
+ const payload = {
836
+ ...body,
837
+ created_by: user.id,
838
+ creator: {
839
+ id: user.id,
840
+ email: user.email || null,
841
+ full_name: user.fullName || null
842
+ },
843
+ task_list_id: String(body.task_list_id || defaultTaskListId),
844
+ status: String(body.status || "Open")
845
+ };
846
+ const createPath = "/api/mobile/tasks";
847
+ const response = await fetchForgeOrThrow(forgeContext, createPath, {
848
+ method: "POST",
849
+ body: JSON.stringify({
850
+ name: String(payload.title || ""),
851
+ description: buildDevNotesReportDescription(payload),
852
+ project_id: project.id,
853
+ completed: mapBugStatusToForge(String(payload.status || "Open")) === "completed",
854
+ assigned_to: payload.assigned_to === null || payload.assigned_to === void 0 ? void 0 : String(payload.assigned_to)
855
+ })
856
+ });
857
+ const taskId = extractForgeTaskId(response.payload);
858
+ if (!taskId) {
859
+ throw new UpstreamForgeError(createPath, forgeContext.baseUrl, {
860
+ ...response,
861
+ status: response.status || 502,
862
+ text: response.text || JSON.stringify({
863
+ error: "Task endpoint succeeded but did not return a task id"
864
+ }),
865
+ contentType: response.contentType || "application/json"
866
+ });
867
+ }
868
+ const tasks = await fetchForgeTasksForProject(forgeContext, project.id);
869
+ const createdTask = tasks.find((task) => String(task.id || "") === taskId) || {
870
+ id: taskId,
871
+ name: payload.title,
872
+ description: buildDevNotesReportDescription(payload),
873
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
874
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
875
+ completed: false
876
+ };
877
+ const report = buildDevNotesReportFromForgeTask(createdTask, null, defaultTaskListId);
878
+ return await jsonResponse(request, options.corsHeaders, report);
879
+ }
880
+ if (resource === "reports" && resourceId && !nested && method === "PATCH") {
881
+ const reportId = decodeURIComponent(resourceId);
882
+ const tasks = await fetchForgeTasksForProject(forgeContext, project.id);
883
+ const existingTask = tasks.find((task) => String(task.id || "") === reportId);
884
+ if (!existingTask) {
885
+ return await jsonResponse(
886
+ request,
887
+ options.corsHeaders,
888
+ { error: "Bug report not found" },
889
+ 404
890
+ );
891
+ }
892
+ const existing = buildDevNotesReportFromForgeTask(
893
+ existingTask,
894
+ reportPatchById.get(reportId) || null,
895
+ defaultTaskListId
896
+ );
897
+ if (!existing) {
898
+ return await jsonResponse(
899
+ request,
900
+ options.corsHeaders,
901
+ { error: "Bug report not found" },
902
+ 404
903
+ );
904
+ }
905
+ const merged = {
906
+ ...existing,
907
+ ...body,
908
+ id: existing.id,
909
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
910
+ resolved_at: body.status === "Resolved" || body.status === "Closed" ? (/* @__PURE__ */ new Date()).toISOString() : existing.resolved_at,
911
+ resolved_by: body.status === "Resolved" || body.status === "Closed" ? body.resolved_by || user.id : existing.resolved_by
912
+ };
913
+ const existingPatch = metadataComments.find(
914
+ (comment) => comment.meta.kind === "report_patch" && String(comment.meta.reportId || "") === reportId
915
+ );
916
+ if (existingPatch) {
917
+ await updateForgeProjectComment(forgeContext, existingPatch.id, "", {
918
+ kind: "report_patch",
919
+ reportId,
920
+ report: merged
921
+ });
922
+ } else {
923
+ await createForgeProjectComment(forgeContext, project.id, "", {
924
+ kind: "report_patch",
925
+ reportId,
926
+ report: merged
927
+ });
928
+ }
929
+ return await jsonResponse(request, options.corsHeaders, merged);
930
+ }
931
+ if (resource === "reports" && resourceId && !nested && method === "DELETE") {
932
+ const reportId = decodeURIComponent(resourceId);
933
+ const existingDelete = metadataComments.find(
934
+ (comment) => comment.meta.kind === "report_deleted" && String(comment.meta.reportId || "") === reportId
935
+ );
936
+ if (existingDelete) {
937
+ await updateForgeProjectComment(forgeContext, existingDelete.id, "", {
938
+ kind: "report_deleted",
939
+ reportId,
940
+ deletedBy: user.id,
941
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
942
+ });
943
+ } else {
944
+ await createForgeProjectComment(forgeContext, project.id, "", {
945
+ kind: "report_deleted",
946
+ reportId,
947
+ deletedBy: user.id,
948
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
949
+ });
950
+ }
951
+ return await jsonResponse(request, options.corsHeaders, { success: true });
952
+ }
953
+ if (resource === "reports" && resourceId && nested === "messages" && method === "GET") {
954
+ const reportId = decodeURIComponent(resourceId);
955
+ const projectMessages = metadataComments.filter(
956
+ (comment) => comment.meta.kind === "message" && String(comment.meta.reportId || "") === reportId
957
+ ).sort((a, b) => a.created_at.localeCompare(b.created_at)).map((comment) => ({
958
+ id: comment.id,
959
+ bug_report_id: reportId,
960
+ author_id: String(comment.meta.authorId || comment.user_id || ""),
961
+ body: comment.body,
962
+ created_at: comment.created_at,
963
+ updated_at: comment.updated_at,
964
+ author: {
965
+ id: String(comment.meta.authorId || comment.user_id || ""),
966
+ email: comment.meta.authorEmail === null || comment.meta.authorEmail === void 0 ? null : String(comment.meta.authorEmail),
967
+ full_name: comment.meta.authorName === null || comment.meta.authorName === void 0 ? null : String(comment.meta.authorName)
968
+ }
969
+ }));
970
+ const legacyTaskMessages = (await fetchForgeTaskComments(forgeContext, reportId)).filter((comment) => String(comment.project_id || "") === "").map((comment) => ({
971
+ id: String(comment.id || ""),
972
+ bug_report_id: reportId,
973
+ author_id: String(comment.user_id || ""),
974
+ body: String(comment.content || ""),
975
+ created_at: String(comment.created_at || (/* @__PURE__ */ new Date()).toISOString()),
976
+ updated_at: String(comment.updated_at || comment.created_at || (/* @__PURE__ */ new Date()).toISOString()),
977
+ author: {
978
+ id: String(comment.user_id || ""),
979
+ email: comment.author_email === null || comment.author_email === void 0 ? null : String(comment.author_email),
980
+ full_name: comment.author_name === null || comment.author_name === void 0 ? null : String(comment.author_name)
981
+ }
982
+ }));
983
+ const merged = [...legacyTaskMessages, ...projectMessages].sort(
984
+ (a, b) => a.created_at.localeCompare(b.created_at)
985
+ );
986
+ return await jsonResponse(request, options.corsHeaders, merged);
987
+ }
988
+ if (resource === "reports" && resourceId && nested === "messages" && method === "POST") {
989
+ const reportId = decodeURIComponent(resourceId);
990
+ const created = await createForgeProjectComment(
991
+ forgeContext,
992
+ project.id,
993
+ String(body.body || "").trim(),
994
+ {
995
+ kind: "message",
996
+ reportId,
997
+ authorId: user.id,
998
+ authorEmail: user.email || null,
999
+ authorName: user.fullName || user.email || ""
1000
+ }
1001
+ );
1002
+ const parsed = parseDevNotesProjectComment(created);
1003
+ if (!parsed) {
1004
+ return await jsonResponse(
1005
+ request,
1006
+ options.corsHeaders,
1007
+ { error: "Failed to create message" },
1008
+ 500
1009
+ );
1010
+ }
1011
+ const message = {
1012
+ id: parsed.id,
1013
+ bug_report_id: reportId,
1014
+ author_id: String(parsed.meta.authorId || user.id),
1015
+ body: parsed.body,
1016
+ created_at: parsed.created_at,
1017
+ updated_at: parsed.updated_at,
1018
+ author: {
1019
+ id: String(parsed.meta.authorId || user.id),
1020
+ email: parsed.meta.authorEmail === null || parsed.meta.authorEmail === void 0 ? null : String(parsed.meta.authorEmail),
1021
+ full_name: parsed.meta.authorName === null || parsed.meta.authorName === void 0 ? null : String(parsed.meta.authorName)
1022
+ }
1023
+ };
1024
+ return await jsonResponse(request, options.corsHeaders, message);
1025
+ }
1026
+ if (resource === "report-types" && method === "GET" && !resourceId) {
1027
+ return await jsonResponse(
1028
+ request,
1029
+ options.corsHeaders,
1030
+ buildReportTypesFromMetadata(metadataComments)
1031
+ );
1032
+ }
1033
+ if (resource === "report-types" && method === "POST" && !resourceId) {
1034
+ const created = await createForgeProjectComment(forgeContext, project.id, "", {
1035
+ kind: "report_type",
1036
+ name: String(body.name || "").trim(),
1037
+ is_default: false,
1038
+ created_by: user.id,
1039
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1040
+ });
1041
+ const parsed = parseDevNotesProjectComment(created);
1042
+ return await jsonResponse(request, options.corsHeaders, {
1043
+ id: parsed?.id || String(created.id || ""),
1044
+ name: String(body.name || "").trim(),
1045
+ is_default: false,
1046
+ created_by: user.id,
1047
+ created_at: parsed?.created_at || (/* @__PURE__ */ new Date()).toISOString()
1048
+ });
1049
+ }
1050
+ if (resource === "report-types" && resourceId && method === "DELETE") {
1051
+ await deleteForgeProjectComment(forgeContext, decodeURIComponent(resourceId));
1052
+ return await jsonResponse(request, options.corsHeaders, { success: true });
1053
+ }
1054
+ if (resource === "task-lists" && method === "GET" && !resourceId) {
1055
+ return await jsonResponse(request, options.corsHeaders, taskLists);
1056
+ }
1057
+ if (resource === "task-lists" && method === "POST" && !resourceId) {
1058
+ const created = await createForgeProjectComment(forgeContext, project.id, "", {
1059
+ kind: "task_list",
1060
+ name: String(body.name || "").trim(),
1061
+ share_slug: generateDevNotesShareSlug(),
1062
+ is_default: false,
1063
+ created_by: user.id,
1064
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1065
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1066
+ });
1067
+ const parsed = parseDevNotesProjectComment(created);
1068
+ return await jsonResponse(request, options.corsHeaders, {
1069
+ id: parsed?.id || String(created.id || ""),
1070
+ name: String(body.name || "").trim(),
1071
+ share_slug: String(parsed?.meta.share_slug || generateDevNotesShareSlug()),
1072
+ is_default: false,
1073
+ created_by: user.id,
1074
+ created_at: parsed?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
1075
+ updated_at: parsed?.updated_at || (/* @__PURE__ */ new Date()).toISOString()
1076
+ });
1077
+ }
1078
+ if (resource === "messages" && resourceId === "read" && method === "POST") {
1079
+ const messageIds = Array.isArray(body.messageIds) ? body.messageIds.map((value) => String(value || "").trim()).filter((value) => Boolean(value)) : [];
1080
+ for (const messageId of Array.from(new Set(messageIds))) {
1081
+ if (readMarkers.has(messageId)) continue;
1082
+ await createForgeProjectComment(forgeContext, project.id, "", {
1083
+ kind: "message_read",
1084
+ messageId,
1085
+ userId: user.id,
1086
+ readAt: (/* @__PURE__ */ new Date()).toISOString()
1087
+ });
1088
+ }
1089
+ return await jsonResponse(request, options.corsHeaders, { success: true });
1090
+ }
1091
+ if (resource === "messages" && resourceId && method === "PATCH") {
1092
+ const current = await fetchForgeCommentById(forgeContext, decodeURIComponent(resourceId));
1093
+ if (!current) {
1094
+ return await jsonResponse(
1095
+ request,
1096
+ options.corsHeaders,
1097
+ { error: "Message not found" },
1098
+ 404
1099
+ );
1100
+ }
1101
+ const parsed = parseDevNotesProjectComment(current);
1102
+ if (!parsed || parsed.meta.kind !== "message") {
1103
+ return await jsonResponse(
1104
+ request,
1105
+ options.corsHeaders,
1106
+ { error: "Message not found" },
1107
+ 404
1108
+ );
1109
+ }
1110
+ const updated = await updateForgeProjectComment(
1111
+ forgeContext,
1112
+ decodeURIComponent(resourceId),
1113
+ String(body.body || "").trim(),
1114
+ parsed.meta
1115
+ );
1116
+ const updatedParsed = parseDevNotesProjectComment(updated);
1117
+ return await jsonResponse(request, options.corsHeaders, {
1118
+ id: updatedParsed?.id || decodeURIComponent(resourceId),
1119
+ bug_report_id: String(parsed.meta.reportId || ""),
1120
+ author_id: String(parsed.meta.authorId || parsed.user_id || ""),
1121
+ body: updatedParsed?.body || String(body.body || "").trim(),
1122
+ created_at: updatedParsed?.created_at || parsed.created_at,
1123
+ updated_at: updatedParsed?.updated_at || (/* @__PURE__ */ new Date()).toISOString(),
1124
+ author: {
1125
+ id: String(parsed.meta.authorId || parsed.user_id || ""),
1126
+ email: parsed.meta.authorEmail === null || parsed.meta.authorEmail === void 0 ? null : String(parsed.meta.authorEmail),
1127
+ full_name: parsed.meta.authorName === null || parsed.meta.authorName === void 0 ? null : String(parsed.meta.authorName)
1128
+ }
1129
+ });
1130
+ }
1131
+ if (resource === "messages" && resourceId && method === "DELETE") {
1132
+ await deleteForgeProjectComment(forgeContext, decodeURIComponent(resourceId));
1133
+ return await jsonResponse(request, options.corsHeaders, { success: true });
1134
+ }
1135
+ if (resource === "unread-counts" && method === "GET") {
1136
+ const counts = {};
1137
+ metadataComments.filter((comment) => comment.meta.kind === "message").forEach((comment) => {
1138
+ const reportId = String(comment.meta.reportId || "").trim();
1139
+ const authorId = String(comment.meta.authorId || "").trim();
1140
+ if (!reportId || authorId === user.id || readMarkers.has(comment.id)) return;
1141
+ counts[reportId] = (counts[reportId] || 0) + 1;
1142
+ });
1143
+ return await jsonResponse(request, options.corsHeaders, counts);
1144
+ }
1145
+ if (resource === "collaborators" && method === "GET") {
1146
+ const tasks = await fetchForgeTasksForProject(forgeContext, project.id);
1147
+ const reports = tasks.map(
1148
+ (task) => buildDevNotesReportFromForgeTask(
1149
+ task,
1150
+ reportPatchById.get(String(task.id || "").trim()) || null,
1151
+ defaultTaskListId
1152
+ )
1153
+ ).filter((item) => Boolean(item));
1154
+ const knownUsers = buildKnownUsers(metadataComments, reports, user);
1155
+ const ids = (url.searchParams.get("ids") || "").split(",").map((value) => value.trim()).filter(Boolean);
1156
+ const resolvedUsers = await maybeResolveUsers(options.resolveUsers, ids);
1157
+ resolvedUsers.forEach((resolved) => knownUsers.set(resolved.id, resolved));
1158
+ const collaborators = ids.length > 0 ? ids.map((id) => knownUsers.get(id)).filter((value) => Boolean(value)) : Array.from(knownUsers.values());
1159
+ return await jsonResponse(
1160
+ request,
1161
+ options.corsHeaders,
1162
+ sortCreators(collaborators)
1163
+ );
1164
+ }
1165
+ return await jsonResponse(request, options.corsHeaders, { error: "Not found" }, 404);
1166
+ } catch (error) {
1167
+ if (error instanceof UpstreamForgeError) {
1168
+ return await passthroughUpstreamResponse(request, options.corsHeaders, error);
1169
+ }
1170
+ return await jsonResponse(
1171
+ request,
1172
+ options.corsHeaders,
1173
+ { error: error instanceof Error ? error.message : "Unexpected error" },
1174
+ 500
1175
+ );
1176
+ }
1177
+ };
1178
+ }
1179
+
1180
+ // src/server/deno.ts
1181
+ function createDenoDevNotesHandler(options) {
1182
+ return createDevNotesServerHandler(options);
1183
+ }
1184
+ // Annotate the CommonJS export names for ESM import in node:
1185
+ 0 && (module.exports = {
1186
+ createDenoDevNotesHandler
1187
+ });