@rwdocs/backstage-plugin-rw-backend 0.1.4 → 0.1.6

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.
Files changed (59) hide show
  1. package/dist/comments/CommentEventPublisher.cjs.js +119 -0
  2. package/dist/comments/CommentEventPublisher.cjs.js.map +1 -0
  3. package/dist/comments/CommentStore.cjs.js +160 -0
  4. package/dist/comments/CommentStore.cjs.js.map +1 -0
  5. package/dist/comments/author.cjs.js +38 -0
  6. package/dist/comments/author.cjs.js.map +1 -0
  7. package/dist/comments/logging.cjs.js +50 -0
  8. package/dist/comments/logging.cjs.js.map +1 -0
  9. package/dist/comments/mapping.cjs.js +32 -0
  10. package/dist/comments/mapping.cjs.js.map +1 -0
  11. package/dist/comments/permissions.cjs.js +24 -0
  12. package/dist/comments/permissions.cjs.js.map +1 -0
  13. package/dist/comments/router.cjs.js +314 -0
  14. package/dist/comments/router.cjs.js.map +1 -0
  15. package/dist/comments/timestamps.cjs.js +23 -0
  16. package/dist/comments/timestamps.cjs.js.map +1 -0
  17. package/dist/comments/types.cjs.js +14 -0
  18. package/dist/comments/types.cjs.js.map +1 -0
  19. package/dist/hub.cjs.js +2 -2
  20. package/dist/hub.cjs.js.map +1 -1
  21. package/dist/inbox/InboxStore.cjs.js +96 -0
  22. package/dist/inbox/InboxStore.cjs.js.map +1 -0
  23. package/dist/inbox/cursor.cjs.js +35 -0
  24. package/dist/inbox/cursor.cjs.js.map +1 -0
  25. package/dist/inbox/inboxRouter.cjs.js +90 -0
  26. package/dist/inbox/inboxRouter.cjs.js.map +1 -0
  27. package/dist/inbox/mapping.cjs.js +35 -0
  28. package/dist/inbox/mapping.cjs.js.map +1 -0
  29. package/dist/inbox/snippet.cjs.js +44 -0
  30. package/dist/inbox/snippet.cjs.js.map +1 -0
  31. package/dist/plugin.cjs.js +129 -34
  32. package/dist/plugin.cjs.js.map +1 -1
  33. package/dist/router.cjs.js +9 -8
  34. package/dist/router.cjs.js.map +1 -1
  35. package/dist/siteIndex/PagesReader.cjs.js +15 -0
  36. package/dist/siteIndex/PagesReader.cjs.js.map +1 -0
  37. package/dist/siteIndex/RegistryStore.cjs.js +20 -0
  38. package/dist/siteIndex/RegistryStore.cjs.js.map +1 -0
  39. package/dist/siteIndex/SectionOwnershipStore.cjs.js +21 -0
  40. package/dist/siteIndex/SectionOwnershipStore.cjs.js.map +1 -0
  41. package/dist/siteIndex/SectionsReader.cjs.js +14 -0
  42. package/dist/siteIndex/SectionsReader.cjs.js.map +1 -0
  43. package/dist/siteIndex/SiteRefreshStore.cjs.js +73 -0
  44. package/dist/siteIndex/SiteRefreshStore.cjs.js.map +1 -0
  45. package/dist/siteIndex/effectiveOwnership.cjs.js +34 -0
  46. package/dist/siteIndex/effectiveOwnership.cjs.js.map +1 -0
  47. package/dist/siteIndex/registryHash.cjs.js +10 -0
  48. package/dist/siteIndex/registryHash.cjs.js.map +1 -0
  49. package/dist/siteIndex/runScan.cjs.js +75 -0
  50. package/dist/siteIndex/runScan.cjs.js.map +1 -0
  51. package/dist/siteIndex/runWorker.cjs.js +81 -0
  52. package/dist/siteIndex/runWorker.cjs.js.map +1 -0
  53. package/dist/siteIndex/schedule.cjs.js +25 -0
  54. package/dist/siteIndex/schedule.cjs.js.map +1 -0
  55. package/migrations/20260621000000_init.js +81 -0
  56. package/package.json +16 -9
  57. package/config.d.ts +0 -56
  58. package/dist/entityPath.cjs.js +0 -12
  59. package/dist/entityPath.cjs.js.map +0 -1
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+ var PromiseRouter = require('express-promise-router');
5
+ var errors = require('@backstage/errors');
6
+ var catalogModel = require('@backstage/catalog-model');
7
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
8
+ var pluginPermissionNode = require('@backstage/plugin-permission-node');
9
+ var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
10
+ var mapping = require('./mapping.cjs.js');
11
+ var author = require('./author.cjs.js');
12
+ var logging = require('./logging.cjs.js');
13
+ var permissions = require('./permissions.cjs.js');
14
+
15
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
16
+
17
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
18
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
19
+
20
+ const MAX_BODY_BYTES = 16 * 1024;
21
+ function validateSiteRef(siteRef) {
22
+ if (typeof siteRef !== "string" || !siteRef) throw new errors.InputError("siteRef is required");
23
+ try {
24
+ catalogModel.parseEntityRef(siteRef);
25
+ } catch {
26
+ throw new errors.InputError("Invalid siteRef");
27
+ }
28
+ }
29
+ function parsePageRef(pageRef) {
30
+ if (typeof pageRef !== "string") throw new errors.InputError("documentId must be a string");
31
+ const i = pageRef.indexOf("#");
32
+ if (i <= 0) throw new errors.InputError(`Malformed documentId: ${pageRef}`);
33
+ return { sectionRef: pageRef.slice(0, i), subpath: pageRef.slice(i + 1) };
34
+ }
35
+ function createCommentsRouter(deps) {
36
+ const router = PromiseRouter__default.default();
37
+ const { store, logger, httpAuth, permissions: permissions$1, catalog } = deps;
38
+ async function checkResourcePermission(permission, credentials, comment) {
39
+ const [decision] = await permissions$1.authorizeConditional([{ permission }], { credentials });
40
+ if (decision.result === pluginPermissionCommon.AuthorizeResult.ALLOW) return true;
41
+ if (decision.result === pluginPermissionCommon.AuthorizeResult.DENY) return false;
42
+ const ruleset = deps.permissionsRegistry.getPermissionRuleset(permissions.commentResourceRef);
43
+ return pluginPermissionNode.createConditionAuthorizer(ruleset)(decision, comment);
44
+ }
45
+ router.use(express__default.default.json({ limit: "64kb" }));
46
+ if (!deps.commentsEnabled) {
47
+ router.get("/comments/config", (_req, res) => res.json({ enabled: false }));
48
+ router.all("/comments", (_req, res) => res.status(404).end());
49
+ router.all("/comments/*", (_req, res) => res.status(404).end());
50
+ return router;
51
+ }
52
+ router.get("/comments/config", (_req, res) => res.json({ enabled: true }));
53
+ async function assertSiteVisible(req, siteRef) {
54
+ const credentials = await httpAuth.credentials(req);
55
+ let entity;
56
+ try {
57
+ entity = await catalog.getEntityByRef(siteRef, { credentials });
58
+ } catch {
59
+ throw new errors.ServiceUnavailableError("Catalog unavailable");
60
+ }
61
+ if (!entity) {
62
+ throw new errors.NotFoundError("Site not found");
63
+ }
64
+ }
65
+ async function callerUserRef(req) {
66
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
67
+ const { userEntityRef } = await deps.userInfo.getUserInfo(credentials);
68
+ return userEntityRef;
69
+ }
70
+ function assertAuthorFloor(userRef, row) {
71
+ if (userRef !== row.author_ref) {
72
+ logging.logCommentOp(logger, {
73
+ kind: "denied",
74
+ op: "mutate",
75
+ permission: "author-floor",
76
+ userEntityRef: userRef
77
+ });
78
+ throw new errors.NotAllowedError("Only the author may perform this operation");
79
+ }
80
+ }
81
+ router.get("/comments", async (req, res) => {
82
+ if (typeof req.query.siteRef !== "string") throw new errors.InputError("siteRef is required");
83
+ const siteRef = req.query.siteRef;
84
+ validateSiteRef(siteRef);
85
+ if (typeof req.query.documentId !== "string") throw new errors.InputError("documentId is required");
86
+ const pageRef = req.query.documentId;
87
+ parsePageRef(pageRef);
88
+ const credentials = await httpAuth.credentials(req);
89
+ const decision = await permissions$1.authorize([{ permission: backstagePluginRwCommon.rwCommentReadPermission }], {
90
+ credentials
91
+ });
92
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
93
+ res.status(403).end();
94
+ return;
95
+ }
96
+ await assertSiteVisible(req, siteRef);
97
+ const rows = await store.list(siteRef, { pageRef });
98
+ const callerRef = await callerUserRef(req).catch(() => void 0);
99
+ res.json(rows.map((r) => mapping.toCommentResponse(r, callerRef)));
100
+ });
101
+ router.post("/comments", async (req, res) => {
102
+ const { siteRef, documentId: pageRef, parentId, body, selectors } = req.body ?? {};
103
+ validateSiteRef(siteRef);
104
+ parsePageRef(pageRef);
105
+ if (!body || typeof body !== "string") throw new errors.InputError("body must be a non-empty string");
106
+ if (Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES)
107
+ throw new errors.InputError("body exceeds maximum length");
108
+ if (selectors !== void 0 && !Array.isArray(selectors))
109
+ throw new errors.InputError("selectors must be an array");
110
+ if (parentId !== void 0 && (typeof parentId !== "string" || parentId.trim() === ""))
111
+ throw new errors.InputError("parentId must be a non-empty string");
112
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
113
+ const decision = await permissions$1.authorize([{ permission: backstagePluginRwCommon.rwCommentCreatePermission }], {
114
+ credentials
115
+ });
116
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
117
+ res.status(403).end();
118
+ return;
119
+ }
120
+ const { authorRef, authorProfile } = await author.resolveAuthor({
121
+ userInfo: deps.userInfo,
122
+ auth: deps.auth,
123
+ catalog,
124
+ credentials
125
+ });
126
+ if (parentId) {
127
+ const parent = await store.get(parentId);
128
+ if (!parent || parent.site_ref !== siteRef || parent.page_ref !== pageRef || parent.parent_id !== null || parent.deleted_at !== null) {
129
+ throw new errors.InputError("Invalid parentId");
130
+ }
131
+ }
132
+ const row = await store.create(siteRef, {
133
+ pageRef,
134
+ parentId,
135
+ authorRef,
136
+ authorProfile,
137
+ body,
138
+ selectors: selectors ?? []
139
+ });
140
+ logging.logCommentOp(logger, { kind: "mutation", op: "create", siteRef, commentId: row.id, parentId });
141
+ res.status(201).json(mapping.toCommentResponse(row, authorRef));
142
+ void deps.publisher?.onCommentCreated(row, authorRef);
143
+ });
144
+ router.get("/comments/:id", async (req, res) => {
145
+ const row = await store.get(req.params.id);
146
+ if (!row || row.deleted_at !== null) throw new errors.NotFoundError("Comment not found");
147
+ const credentials = await httpAuth.credentials(req);
148
+ const decision = await permissions$1.authorize([{ permission: backstagePluginRwCommon.rwCommentReadPermission }], {
149
+ credentials
150
+ });
151
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
152
+ res.status(403).end();
153
+ return;
154
+ }
155
+ await assertSiteVisible(req, row.site_ref);
156
+ const callerRef = await callerUserRef(req).catch(() => void 0);
157
+ res.json(mapping.toCommentResponse(row, callerRef));
158
+ });
159
+ router.patch("/comments/:id", async (req, res) => {
160
+ const row = await store.get(req.params.id);
161
+ if (!row) throw new errors.NotFoundError("Comment not found");
162
+ const { body, status, selectors } = req.body ?? {};
163
+ if (status !== void 0 && status !== "open" && status !== "resolved") {
164
+ throw new errors.InputError("Invalid status");
165
+ }
166
+ if (body !== void 0 && (typeof body !== "string" || !body))
167
+ throw new errors.InputError("body must be a non-empty string");
168
+ if (body !== void 0 && Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES)
169
+ throw new errors.InputError("body exceeds maximum length");
170
+ if (selectors !== void 0 && !Array.isArray(selectors))
171
+ throw new errors.InputError("selectors must be an array");
172
+ if (body === void 0 && status === void 0 && selectors === void 0)
173
+ throw new errors.InputError("No editable fields provided");
174
+ const editing = body !== void 0 || selectors !== void 0;
175
+ const userRef = await callerUserRef(req);
176
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
177
+ const commentAsResource = mapping.toCommentResponse(row, userRef);
178
+ if (row.deleted_at !== null) {
179
+ if (editing || status !== "open") throw new errors.InputError("Cannot edit a deleted comment");
180
+ const frameworkAllowed = await checkResourcePermission(
181
+ backstagePluginRwCommon.rwCommentDeletePermission,
182
+ credentials,
183
+ commentAsResource
184
+ );
185
+ if (!frameworkAllowed) {
186
+ logging.logCommentOp(logger, {
187
+ kind: "denied",
188
+ op: "mutate",
189
+ permission: "rwComment.delete",
190
+ userEntityRef: userRef
191
+ });
192
+ res.status(403).end();
193
+ return;
194
+ }
195
+ assertAuthorFloor(userRef, row);
196
+ const restored = await store.transaction(async (tx) => {
197
+ const fresh = await store.get(row.id, { executor: tx, forUpdate: true });
198
+ if (!fresh || fresh.deleted_at === null) {
199
+ throw new errors.NotFoundError("Comment not found");
200
+ }
201
+ return store.restore(row.id, tx);
202
+ });
203
+ logging.logCommentOp(logger, {
204
+ kind: "mutation",
205
+ op: "restore",
206
+ siteRef: row.site_ref,
207
+ commentId: row.id
208
+ });
209
+ res.json(mapping.toCommentResponse(restored, userRef));
210
+ return;
211
+ }
212
+ if (editing) {
213
+ const frameworkAllowed = await checkResourcePermission(
214
+ backstagePluginRwCommon.rwCommentEditPermission,
215
+ credentials,
216
+ commentAsResource
217
+ );
218
+ if (!frameworkAllowed) {
219
+ logging.logCommentOp(logger, {
220
+ kind: "denied",
221
+ op: "mutate",
222
+ permission: "rwComment.edit",
223
+ userEntityRef: userRef
224
+ });
225
+ res.status(403).end();
226
+ return;
227
+ }
228
+ assertAuthorFloor(userRef, row);
229
+ }
230
+ if (status !== void 0) {
231
+ if (row.parent_id !== null) throw new errors.InputError("Cannot resolve a reply");
232
+ const frameworkAllowed = await checkResourcePermission(
233
+ backstagePluginRwCommon.rwCommentResolvePermission,
234
+ credentials,
235
+ commentAsResource
236
+ );
237
+ if (!frameworkAllowed) {
238
+ logging.logCommentOp(logger, {
239
+ kind: "denied",
240
+ op: "mutate",
241
+ permission: "rwComment.resolve",
242
+ userEntityRef: userRef
243
+ });
244
+ res.status(403).end();
245
+ return;
246
+ }
247
+ }
248
+ const updated = await store.update(row.id, {
249
+ ...body !== void 0 ? { body } : {},
250
+ ...selectors !== void 0 ? { selectors } : {},
251
+ ...status !== void 0 ? { status } : {},
252
+ resolverRef: userRef
253
+ });
254
+ logging.logCommentOp(logger, {
255
+ kind: "mutation",
256
+ op: status ? "resolve" : "edit",
257
+ siteRef: row.site_ref,
258
+ commentId: row.id
259
+ });
260
+ res.json(mapping.toCommentResponse(updated, userRef));
261
+ if (status === "resolved") {
262
+ const resolverName = await author.resolveAuthor({
263
+ userInfo: deps.userInfo,
264
+ auth: deps.auth,
265
+ catalog,
266
+ credentials
267
+ }).then((a) => a.authorProfile?.displayName ?? void 0).catch(() => void 0);
268
+ void deps.publisher?.onCommentResolved(updated, userRef, resolverName);
269
+ }
270
+ });
271
+ router.delete("/comments/:id", async (req, res) => {
272
+ const row = await store.get(req.params.id);
273
+ if (!row || row.parent_id === null || row.deleted_at !== null)
274
+ throw new errors.NotFoundError("Reply not found");
275
+ const userRef = await callerUserRef(req);
276
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
277
+ const commentAsResource = mapping.toCommentResponse(row, userRef);
278
+ const frameworkAllowed = await checkResourcePermission(
279
+ backstagePluginRwCommon.rwCommentDeletePermission,
280
+ credentials,
281
+ commentAsResource
282
+ );
283
+ if (!frameworkAllowed) {
284
+ logging.logCommentOp(logger, {
285
+ kind: "denied",
286
+ op: "mutate",
287
+ permission: "rwComment.delete",
288
+ userEntityRef: userRef
289
+ });
290
+ res.status(403).end();
291
+ return;
292
+ }
293
+ assertAuthorFloor(userRef, row);
294
+ const deleted = await store.transaction(async (tx) => {
295
+ const fresh = await store.get(row.id, { executor: tx, forUpdate: true });
296
+ if (!fresh || fresh.parent_id === null || fresh.deleted_at !== null) {
297
+ throw new errors.NotFoundError("Reply not found");
298
+ }
299
+ return store.softDelete(row.id, tx);
300
+ });
301
+ logging.logCommentOp(logger, {
302
+ kind: "mutation",
303
+ op: "delete",
304
+ siteRef: row.site_ref,
305
+ commentId: row.id
306
+ });
307
+ res.json(mapping.toCommentResponse(deleted, userRef));
308
+ });
309
+ return router;
310
+ }
311
+
312
+ exports.createCommentsRouter = createCommentsRouter;
313
+ exports.parsePageRef = parsePageRef;
314
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/comments/router.ts"],"sourcesContent":["import express from \"express\";\nimport PromiseRouter from \"express-promise-router\";\nimport type { Router } from \"express\";\nimport {\n InputError,\n NotAllowedError,\n NotFoundError,\n ServiceUnavailableError,\n} from \"@backstage/errors\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport {\n AuthService,\n BackstageCredentials,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n PermissionsRegistryService,\n UserInfoService,\n} from \"@backstage/backend-plugin-api\";\nimport type { CatalogService } from \"@backstage/plugin-catalog-node\";\nimport { AuthorizeResult } from \"@backstage/plugin-permission-common\";\nimport type { ResourcePermission } from \"@backstage/plugin-permission-common\";\nimport { createConditionAuthorizer } from \"@backstage/plugin-permission-node\";\nimport {\n rwCommentReadPermission,\n rwCommentCreatePermission,\n rwCommentEditPermission,\n rwCommentResolvePermission,\n rwCommentDeletePermission,\n} from \"@rwdocs/backstage-plugin-rw-common\";\nimport { CommentStore } from \"./CommentStore\";\nimport { toCommentResponse } from \"./mapping\";\nimport type { CommentResponse } from \"./mapping\";\nimport { resolveAuthor } from \"./author\";\nimport { logCommentOp } from \"./logging\";\nimport { commentResourceRef } from \"./permissions\";\nimport type { CommentEventPublisher } from \"./CommentEventPublisher\";\n\nconst MAX_BODY_BYTES = 16 * 1024;\n\n/**\n * Validates that `siteRef` is a well-formed Backstage entity ref string.\n * Throws InputError(400) on any parse failure so callers receive a 400, not 503.\n */\nfunction validateSiteRef(siteRef: unknown): asserts siteRef is string {\n if (typeof siteRef !== \"string\" || !siteRef) throw new InputError(\"siteRef is required\");\n try {\n parseEntityRef(siteRef);\n } catch {\n throw new InputError(\"Invalid siteRef\");\n }\n}\n\nexport interface CommentsRouterDeps {\n store: CommentStore;\n logger: LoggerService;\n httpAuth: HttpAuthService;\n auth: AuthService;\n userInfo: UserInfoService;\n permissions: PermissionsService;\n permissionsRegistry: PermissionsRegistryService;\n catalog: CatalogService;\n commentsEnabled: boolean;\n /** Optional: when present, comment create/resolve fire-and-forget a domain event.\n * Absent in tests that don't exercise notifications. */\n publisher?: CommentEventPublisher;\n}\n\n/** Split a viewer pageRef (\"<sectionRef>#<subpath>\"); throws InputError(400) on a\n * non-string, missing or leading '#'. The error messages intentionally say\n * \"documentId\" — they are returned verbatim to the viewer, which only knows the\n * frozen wire field name (not the internal `pageRef`). */\nexport function parsePageRef(pageRef: unknown): { sectionRef: string; subpath: string } {\n if (typeof pageRef !== \"string\") throw new InputError(\"documentId must be a string\");\n const i = pageRef.indexOf(\"#\");\n if (i <= 0) throw new InputError(`Malformed documentId: ${pageRef}`);\n return { sectionRef: pageRef.slice(0, i), subpath: pageRef.slice(i + 1) };\n}\n\nexport function createCommentsRouter(deps: CommentsRouterDeps): Router {\n const router = PromiseRouter();\n const { store, logger, httpAuth, permissions, catalog } = deps;\n\n /**\n * Framework resource-permission check only. The caller must ALSO call\n * `assertAuthorFloor` where author ownership is required (edit, delete,\n * restore) — this function does not enforce that floor.\n *\n * Calls authorizeConditional and evaluates the returned PolicyDecision in-memory:\n * - ALLOW → true\n * - DENY → false\n * - CONDITIONAL → evaluates the conditions tree against `comment` using the registered ruleset\n */\n async function checkResourcePermission(\n permission: ResourcePermission,\n credentials: BackstageCredentials,\n comment: CommentResponse,\n ): Promise<boolean> {\n const [decision] = await permissions.authorizeConditional([{ permission }], { credentials });\n if (decision.result === AuthorizeResult.ALLOW) return true;\n if (decision.result === AuthorizeResult.DENY) return false;\n // CONDITIONAL: evaluate conditions tree against the in-memory comment\n const ruleset = deps.permissionsRegistry.getPermissionRuleset(commentResourceRef);\n return createConditionAuthorizer(ruleset)(decision, comment);\n }\n\n router.use(express.json({ limit: \"64kb\" }));\n\n if (!deps.commentsEnabled) {\n router.get(\"/comments/config\", (_req, res) => res.json({ enabled: false }));\n router.all(\"/comments\", (_req, res) => res.status(404).end());\n router.all(\"/comments/*\", (_req, res) => res.status(404).end());\n return router;\n }\n\n router.get(\"/comments/config\", (_req, res) => res.json({ enabled: true }));\n\n /**\n * Read authorization is intentionally governed by the host `siteRef`'s read scope.\n * The host site entity governs all comment content it hosts — a caller who can read\n * the site may read its comments, regardless of which section (section_ref) a comment\n * belongs to. The stored `section_ref` is for future cross-section / entity-scoped\n * querying (see `ListFilter`), not a security boundary. This is by design.\n */\n // Entity-read scope: caller must be able to read the host site entity.\n async function assertSiteVisible(req: any, siteRef: string): Promise<void> {\n const credentials = await httpAuth.credentials(req);\n let entity: Awaited<ReturnType<typeof catalog.getEntityByRef>>;\n try {\n entity = await catalog.getEntityByRef(siteRef, { credentials });\n } catch {\n throw new ServiceUnavailableError(\"Catalog unavailable\");\n }\n if (!entity) {\n throw new NotFoundError(\"Site not found\");\n }\n }\n\n async function callerUserRef(req: any): Promise<string> {\n const credentials = await httpAuth.credentials(req, { allow: [\"user\"] });\n const { userEntityRef } = await deps.userInfo.getUserInfo(credentials);\n return userEntityRef;\n }\n\n function assertAuthorFloor(userRef: string, row: { author_ref: string }): void {\n if (userRef !== row.author_ref) {\n logCommentOp(logger, {\n kind: \"denied\",\n op: \"mutate\",\n permission: \"author-floor\",\n userEntityRef: userRef,\n });\n throw new NotAllowedError(\"Only the author may perform this operation\");\n }\n }\n\n router.get(\"/comments\", async (req, res) => {\n if (typeof req.query.siteRef !== \"string\") throw new InputError(\"siteRef is required\");\n const siteRef = req.query.siteRef;\n validateSiteRef(siteRef);\n if (typeof req.query.documentId !== \"string\") throw new InputError(\"documentId is required\");\n const pageRef = req.query.documentId; // viewer wire: req.query.documentId → internal pageRef\n parsePageRef(pageRef); // 400 guard\n const credentials = await httpAuth.credentials(req);\n const decision = await permissions.authorize([{ permission: rwCommentReadPermission }], {\n credentials,\n });\n if (decision[0].result !== AuthorizeResult.ALLOW) {\n res.status(403).end();\n return;\n }\n await assertSiteVisible(req, siteRef);\n const rows = await store.list(siteRef, { pageRef });\n const callerRef = await callerUserRef(req).catch(() => undefined);\n res.json(rows.map((r) => toCommentResponse(r, callerRef)));\n });\n\n router.post(\"/comments\", async (req, res) => {\n // documentId is the viewer wire field name — kept frozen on the wire and\n // renamed to the internal pageRef at the destructure so it never leaks into internals.\n const { siteRef, documentId: pageRef, parentId, body, selectors } = req.body ?? {};\n validateSiteRef(siteRef);\n parsePageRef(pageRef); // 400 guard\n if (!body || typeof body !== \"string\") throw new InputError(\"body must be a non-empty string\");\n if (Buffer.byteLength(body, \"utf8\") > MAX_BODY_BYTES)\n throw new InputError(\"body exceeds maximum length\");\n if (selectors !== undefined && !Array.isArray(selectors))\n throw new InputError(\"selectors must be an array\");\n if (parentId !== undefined && (typeof parentId !== \"string\" || parentId.trim() === \"\"))\n throw new InputError(\"parentId must be a non-empty string\");\n const credentials = await httpAuth.credentials(req, { allow: [\"user\"] });\n const decision = await permissions.authorize([{ permission: rwCommentCreatePermission }], {\n credentials,\n });\n if (decision[0].result !== AuthorizeResult.ALLOW) {\n res.status(403).end();\n return;\n }\n\n const { authorRef, authorProfile } = await resolveAuthor({\n userInfo: deps.userInfo,\n auth: deps.auth,\n catalog,\n credentials,\n });\n if (parentId) {\n const parent = await store.get(parentId);\n if (\n !parent ||\n parent.site_ref !== siteRef ||\n parent.page_ref !== pageRef ||\n parent.parent_id !== null ||\n parent.deleted_at !== null\n ) {\n throw new InputError(\"Invalid parentId\");\n }\n }\n const row = await store.create(siteRef, {\n pageRef,\n parentId,\n authorRef,\n authorProfile,\n body,\n selectors: selectors ?? [],\n });\n logCommentOp(logger, { kind: \"mutation\", op: \"create\", siteRef, commentId: row.id, parentId });\n res.status(201).json(toCommentResponse(row, authorRef));\n void deps.publisher?.onCommentCreated(row, authorRef);\n });\n\n router.get(\"/comments/:id\", async (req, res) => {\n const row = await store.get(req.params.id);\n if (!row || row.deleted_at !== null) throw new NotFoundError(\"Comment not found\");\n const credentials = await httpAuth.credentials(req);\n const decision = await permissions.authorize([{ permission: rwCommentReadPermission }], {\n credentials,\n });\n if (decision[0].result !== AuthorizeResult.ALLOW) {\n res.status(403).end();\n return;\n }\n await assertSiteVisible(req, row.site_ref);\n const callerRef = await callerUserRef(req).catch(() => undefined);\n res.json(toCommentResponse(row, callerRef));\n });\n\n router.patch(\"/comments/:id\", async (req, res) => {\n const row = await store.get(req.params.id);\n if (!row) throw new NotFoundError(\"Comment not found\");\n const { body, status, selectors } = req.body ?? {};\n if (status !== undefined && status !== \"open\" && status !== \"resolved\") {\n throw new InputError(\"Invalid status\");\n }\n if (body !== undefined && (typeof body !== \"string\" || !body))\n throw new InputError(\"body must be a non-empty string\");\n if (body !== undefined && Buffer.byteLength(body, \"utf8\") > MAX_BODY_BYTES)\n throw new InputError(\"body exceeds maximum length\");\n if (selectors !== undefined && !Array.isArray(selectors))\n throw new InputError(\"selectors must be an array\");\n if (body === undefined && status === undefined && selectors === undefined)\n throw new InputError(\"No editable fields provided\");\n const editing = body !== undefined || selectors !== undefined;\n const userRef = await callerUserRef(req);\n const credentials = await httpAuth.credentials(req, { allow: [\"user\"] });\n const commentAsResource = toCommentResponse(row, userRef);\n\n if (row.deleted_at !== null) {\n // Only restore (status:'open') is legal on a deleted row.\n if (editing || status !== \"open\") throw new InputError(\"Cannot edit a deleted comment\");\n // Restore reuses rwCommentDeletePermission — the same capability gate as\n // deletion (undoing a delete is as powerful as performing one) — plus the\n // author floor so only the original author can un-delete their own comment.\n const frameworkAllowed = await checkResourcePermission(\n rwCommentDeletePermission,\n credentials,\n commentAsResource,\n );\n if (!frameworkAllowed) {\n logCommentOp(logger, {\n kind: \"denied\",\n op: \"mutate\",\n permission: \"rwComment.delete\",\n userEntityRef: userRef,\n });\n res.status(403).end();\n return;\n }\n assertAuthorFloor(userRef, row);\n // Same TOCTOU guard as DELETE: re-read under a row lock inside a transaction\n // and re-assert the row is still deleted before conditionally restoring.\n // Permission + author-floor checks remain outside the transaction.\n const restored = await store.transaction(async (tx) => {\n const fresh = await store.get(row.id, { executor: tx, forUpdate: true });\n if (!fresh || fresh.deleted_at === null) {\n throw new NotFoundError(\"Comment not found\");\n }\n return store.restore(row.id, tx);\n });\n logCommentOp(logger, {\n kind: \"mutation\",\n op: \"restore\",\n siteRef: row.site_ref,\n commentId: row.id,\n });\n res.json(toCommentResponse(restored!, userRef));\n return;\n }\n\n if (editing) {\n // Framework check (rwCommentEditPermission) + author floor (both must pass).\n const frameworkAllowed = await checkResourcePermission(\n rwCommentEditPermission,\n credentials,\n commentAsResource,\n );\n if (!frameworkAllowed) {\n logCommentOp(logger, {\n kind: \"denied\",\n op: \"mutate\",\n permission: \"rwComment.edit\",\n userEntityRef: userRef,\n });\n res.status(403).end();\n return;\n }\n assertAuthorFloor(userRef, row);\n }\n\n if (status !== undefined) {\n // Replies cannot be resolved/reopened — canResolve is advertised false for replies.\n if (row.parent_id !== null) throw new InputError(\"Cannot resolve a reply\");\n // status change on a live row = resolve/reopen (collaborative — no floor, but framework applies)\n const frameworkAllowed = await checkResourcePermission(\n rwCommentResolvePermission,\n credentials,\n commentAsResource,\n );\n if (!frameworkAllowed) {\n logCommentOp(logger, {\n kind: \"denied\",\n op: \"mutate\",\n permission: \"rwComment.resolve\",\n userEntityRef: userRef,\n });\n res.status(403).end();\n return;\n }\n }\n\n const updated = await store.update(row.id, {\n ...(body !== undefined ? { body } : {}),\n ...(selectors !== undefined ? { selectors } : {}),\n ...(status !== undefined ? { status } : {}),\n resolverRef: userRef,\n });\n logCommentOp(logger, {\n kind: \"mutation\",\n op: status ? \"resolve\" : \"edit\",\n siteRef: row.site_ref,\n commentId: row.id,\n });\n res.json(toCommentResponse(updated!, userRef));\n // Notify participants only on resolve; reopens/edits aren't a thread-ending event worth a push (spec §6 noise model).\n if (status === \"resolved\") {\n // Resolve the display name of the resolver (best-effort; falls back to parsed entity name).\n const resolverName = await resolveAuthor({\n userInfo: deps.userInfo,\n auth: deps.auth,\n catalog,\n credentials,\n })\n .then((a) => a.authorProfile?.displayName ?? undefined)\n .catch(() => undefined);\n void deps.publisher?.onCommentResolved(updated!, userRef, resolverName);\n }\n });\n\n router.delete(\"/comments/:id\", async (req, res) => {\n const row = await store.get(req.params.id);\n if (!row || row.parent_id === null || row.deleted_at !== null)\n throw new NotFoundError(\"Reply not found\");\n const userRef = await callerUserRef(req);\n const credentials = await httpAuth.credentials(req, { allow: [\"user\"] });\n const commentAsResource = toCommentResponse(row, userRef);\n // Framework check (rwCommentDeletePermission) — must pass alongside author floor.\n const frameworkAllowed = await checkResourcePermission(\n rwCommentDeletePermission,\n credentials,\n commentAsResource,\n );\n if (!frameworkAllowed) {\n logCommentOp(logger, {\n kind: \"denied\",\n op: \"mutate\",\n permission: \"rwComment.delete\",\n userEntityRef: userRef,\n });\n res.status(403).end();\n return;\n }\n assertAuthorFloor(userRef, row);\n // State-sensitive re-read + write inside a transaction closes the TOCTOU race\n // between the `row` read above and the mutation: re-assert the reply is still\n // live under a row lock, then conditionally soft-delete. The permission +\n // author-floor checks stay OUTSIDE the transaction so we never hold a DB lock\n // across external permission-service / auth calls.\n const deleted = await store.transaction(async (tx) => {\n const fresh = await store.get(row.id, { executor: tx, forUpdate: true });\n if (!fresh || fresh.parent_id === null || fresh.deleted_at !== null) {\n throw new NotFoundError(\"Reply not found\");\n }\n return store.softDelete(row.id, tx);\n });\n logCommentOp(logger, {\n kind: \"mutation\",\n op: \"delete\",\n siteRef: row.site_ref,\n commentId: row.id,\n });\n // Return the full soft-deleted row so the viewer can evict the reply from its\n // local cache by documentId (viewer wire field, emitted by toCommentResponse).\n // A 204 No Content would leave the viewer without the documentId it needs to\n // locate and remove the reply from state.\n res.json(toCommentResponse(deleted!, userRef));\n });\n\n return router;\n}\n"],"names":["InputError","parseEntityRef","PromiseRouter","permissions","AuthorizeResult","commentResourceRef","createConditionAuthorizer","express","ServiceUnavailableError","NotFoundError","logCommentOp","NotAllowedError","rwCommentReadPermission","toCommentResponse","rwCommentCreatePermission","resolveAuthor","rwCommentDeletePermission","rwCommentEditPermission","rwCommentResolvePermission"],"mappings":";;;;;;;;;;;;;;;;;;;AAsCA,MAAM,iBAAiB,EAAA,GAAK,IAAA;AAM5B,SAAS,gBAAgB,OAAA,EAA6C;AACpE,EAAA,IAAI,OAAO,YAAY,QAAA,IAAY,CAAC,SAAS,MAAM,IAAIA,kBAAW,qBAAqB,CAAA;AACvF,EAAA,IAAI;AACF,IAAAC,2BAAA,CAAe,OAAO,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAID,kBAAW,iBAAiB,CAAA;AAAA,EACxC;AACF;AAqBO,SAAS,aAAa,OAAA,EAA2D;AACtF,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,EAAU,MAAM,IAAIA,kBAAW,6BAA6B,CAAA;AACnF,EAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC7B,EAAA,IAAI,KAAK,CAAA,EAAG,MAAM,IAAIA,iBAAA,CAAW,CAAA,sBAAA,EAAyB,OAAO,CAAA,CAAE,CAAA;AACnE,EAAA,OAAO,EAAE,UAAA,EAAY,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,EAAG,OAAA,EAAS,OAAA,CAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,EAAE;AAC1E;AAEO,SAAS,qBAAqB,IAAA,EAAkC;AACrE,EAAA,MAAM,SAASE,8BAAA,EAAc;AAC7B,EAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAA,eAAUC,aAAA,EAAa,SAAQ,GAAI,IAAA;AAY1D,EAAA,eAAe,uBAAA,CACb,UAAA,EACA,WAAA,EACA,OAAA,EACkB;AAClB,IAAA,MAAM,CAAC,QAAQ,CAAA,GAAI,MAAMA,aAAA,CAAY,oBAAA,CAAqB,CAAC,EAAE,UAAA,EAAY,CAAA,EAAG,EAAE,aAAa,CAAA;AAC3F,IAAA,IAAI,QAAA,CAAS,MAAA,KAAWC,sCAAA,CAAgB,KAAA,EAAO,OAAO,IAAA;AACtD,IAAA,IAAI,QAAA,CAAS,MAAA,KAAWA,sCAAA,CAAgB,IAAA,EAAM,OAAO,KAAA;AAErD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,mBAAA,CAAoB,oBAAA,CAAqBC,8BAAkB,CAAA;AAChF,IAAA,OAAOC,8CAAA,CAA0B,OAAO,CAAA,CAAE,QAAA,EAAU,OAAO,CAAA;AAAA,EAC7D;AAEA,EAAA,MAAA,CAAO,IAAIC,wBAAA,CAAQ,IAAA,CAAK,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAC,CAAA;AAE1C,EAAA,IAAI,CAAC,KAAK,eAAA,EAAiB;AACzB,IAAA,MAAA,CAAO,GAAA,CAAI,kBAAA,EAAoB,CAAC,IAAA,EAAM,GAAA,KAAQ,GAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,KAAA,EAAO,CAAC,CAAA;AAC1E,IAAA,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,CAAC,IAAA,EAAM,GAAA,KAAQ,IAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAK,CAAA;AAC5D,IAAA,MAAA,CAAO,GAAA,CAAI,aAAA,EAAe,CAAC,IAAA,EAAM,GAAA,KAAQ,IAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAK,CAAA;AAC9D,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,GAAA,CAAI,kBAAA,EAAoB,CAAC,IAAA,EAAM,GAAA,KAAQ,GAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,IAAA,EAAM,CAAC,CAAA;AAUzE,EAAA,eAAe,iBAAA,CAAkB,KAAU,OAAA,EAAgC;AACzE,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,MAAM,OAAA,CAAQ,cAAA,CAAe,OAAA,EAAS,EAAE,aAAa,CAAA;AAAA,IAChE,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,IAAIC,+BAAwB,qBAAqB,CAAA;AAAA,IACzD;AACA,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAM,IAAIC,qBAAc,gBAAgB,CAAA;AAAA,IAC1C;AAAA,EACF;AAEA,EAAA,eAAe,cAAc,GAAA,EAA2B;AACtD,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AACvE,IAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,IAAA,CAAK,QAAA,CAAS,YAAY,WAAW,CAAA;AACrE,IAAA,OAAO,aAAA;AAAA,EACT;AAEA,EAAA,SAAS,iBAAA,CAAkB,SAAiB,GAAA,EAAmC;AAC7E,IAAA,IAAI,OAAA,KAAY,IAAI,UAAA,EAAY;AAC9B,MAAAC,oBAAA,CAAa,MAAA,EAAQ;AAAA,QACnB,IAAA,EAAM,QAAA;AAAA,QACN,EAAA,EAAI,QAAA;AAAA,QACJ,UAAA,EAAY,cAAA;AAAA,QACZ,aAAA,EAAe;AAAA,OAChB,CAAA;AACD,MAAA,MAAM,IAAIC,uBAAgB,4CAA4C,CAAA;AAAA,IACxE;AAAA,EACF;AAEA,EAAA,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC1C,IAAA,IAAI,OAAO,IAAI,KAAA,CAAM,OAAA,KAAY,UAAU,MAAM,IAAIX,kBAAW,qBAAqB,CAAA;AACrF,IAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,OAAA;AAC1B,IAAA,eAAA,CAAgB,OAAO,CAAA;AACvB,IAAA,IAAI,OAAO,IAAI,KAAA,CAAM,UAAA,KAAe,UAAU,MAAM,IAAIA,kBAAW,wBAAwB,CAAA;AAC3F,IAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,UAAA;AAC1B,IAAA,YAAA,CAAa,OAAO,CAAA;AACpB,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,MAAM,QAAA,GAAW,MAAMG,aAAA,CAAY,SAAA,CAAU,CAAC,EAAE,UAAA,EAAYS,+CAAA,EAAyB,CAAA,EAAG;AAAA,MACtF;AAAA,KACD,CAAA;AACD,IAAA,IAAI,QAAA,CAAS,CAAC,CAAA,CAAE,MAAA,KAAWR,uCAAgB,KAAA,EAAO;AAChD,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,iBAAA,CAAkB,KAAK,OAAO,CAAA;AACpC,IAAA,MAAM,OAAO,MAAM,KAAA,CAAM,KAAK,OAAA,EAAS,EAAE,SAAS,CAAA;AAClD,IAAA,MAAM,YAAY,MAAM,aAAA,CAAc,GAAG,CAAA,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAChE,IAAA,GAAA,CAAI,IAAA,CAAK,KAAK,GAAA,CAAI,CAAC,MAAMS,yBAAA,CAAkB,CAAA,EAAG,SAAS,CAAC,CAAC,CAAA;AAAA,EAC3D,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAG3C,IAAA,MAAM,EAAE,OAAA,EAAS,UAAA,EAAY,OAAA,EAAS,QAAA,EAAU,MAAM,SAAA,EAAU,GAAI,GAAA,CAAI,IAAA,IAAQ,EAAC;AACjF,IAAA,eAAA,CAAgB,OAAO,CAAA;AACvB,IAAA,YAAA,CAAa,OAAO,CAAA;AACpB,IAAA,IAAI,CAAC,QAAQ,OAAO,IAAA,KAAS,UAAU,MAAM,IAAIb,kBAAW,iCAAiC,CAAA;AAC7F,IAAA,IAAI,MAAA,CAAO,UAAA,CAAW,IAAA,EAAM,MAAM,CAAA,GAAI,cAAA;AACpC,MAAA,MAAM,IAAIA,kBAAW,6BAA6B,CAAA;AACpD,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,CAAC,KAAA,CAAM,QAAQ,SAAS,CAAA;AACrD,MAAA,MAAM,IAAIA,kBAAW,4BAA4B,CAAA;AACnD,IAAA,IAAI,aAAa,MAAA,KAAc,OAAO,aAAa,QAAA,IAAY,QAAA,CAAS,MAAK,KAAM,EAAA,CAAA;AACjF,MAAA,MAAM,IAAIA,kBAAW,qCAAqC,CAAA;AAC5D,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AACvE,IAAA,MAAM,QAAA,GAAW,MAAMG,aAAA,CAAY,SAAA,CAAU,CAAC,EAAE,UAAA,EAAYW,iDAAA,EAA2B,CAAA,EAAG;AAAA,MACxF;AAAA,KACD,CAAA;AACD,IAAA,IAAI,QAAA,CAAS,CAAC,CAAA,CAAE,MAAA,KAAWV,uCAAgB,KAAA,EAAO;AAChD,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,SAAA,EAAW,aAAA,EAAc,GAAI,MAAMW,oBAAA,CAAc;AAAA,MACvD,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAA;AAAA,MACA;AAAA,KACD,CAAA;AACD,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,GAAA,CAAI,QAAQ,CAAA;AACvC,MAAA,IACE,CAAC,MAAA,IACD,MAAA,CAAO,QAAA,KAAa,OAAA,IACpB,MAAA,CAAO,QAAA,KAAa,OAAA,IACpB,MAAA,CAAO,SAAA,KAAc,IAAA,IACrB,MAAA,CAAO,eAAe,IAAA,EACtB;AACA,QAAA,MAAM,IAAIf,kBAAW,kBAAkB,CAAA;AAAA,MACzC;AAAA,IACF;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,MAAA,CAAO,OAAA,EAAS;AAAA,MACtC,OAAA;AAAA,MACA,QAAA;AAAA,MACA,SAAA;AAAA,MACA,aAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA,EAAW,aAAa;AAAC,KAC1B,CAAA;AACD,IAAAU,oBAAA,CAAa,MAAA,EAAQ,EAAE,IAAA,EAAM,UAAA,EAAY,EAAA,EAAI,QAAA,EAAU,OAAA,EAAS,SAAA,EAAW,GAAA,CAAI,EAAA,EAAI,QAAA,EAAU,CAAA;AAC7F,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAKG,yBAAA,CAAkB,GAAA,EAAK,SAAS,CAAC,CAAA;AACtD,IAAA,KAAK,IAAA,CAAK,SAAA,EAAW,gBAAA,CAAiB,GAAA,EAAK,SAAS,CAAA;AAAA,EACtD,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,eAAA,EAAiB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC9C,IAAA,MAAM,MAAM,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,OAAO,EAAE,CAAA;AACzC,IAAA,IAAI,CAAC,OAAO,GAAA,CAAI,UAAA,KAAe,MAAM,MAAM,IAAIJ,qBAAc,mBAAmB,CAAA;AAChF,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,MAAM,QAAA,GAAW,MAAMN,aAAA,CAAY,SAAA,CAAU,CAAC,EAAE,UAAA,EAAYS,+CAAA,EAAyB,CAAA,EAAG;AAAA,MACtF;AAAA,KACD,CAAA;AACD,IAAA,IAAI,QAAA,CAAS,CAAC,CAAA,CAAE,MAAA,KAAWR,uCAAgB,KAAA,EAAO;AAChD,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,iBAAA,CAAkB,GAAA,EAAK,GAAA,CAAI,QAAQ,CAAA;AACzC,IAAA,MAAM,YAAY,MAAM,aAAA,CAAc,GAAG,CAAA,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAChE,IAAA,GAAA,CAAI,IAAA,CAAKS,yBAAA,CAAkB,GAAA,EAAK,SAAS,CAAC,CAAA;AAAA,EAC5C,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,KAAA,CAAM,eAAA,EAAiB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAChD,IAAA,MAAM,MAAM,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,OAAO,EAAE,CAAA;AACzC,IAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAIJ,qBAAc,mBAAmB,CAAA;AACrD,IAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,WAAU,GAAI,GAAA,CAAI,QAAQ,EAAC;AACjD,IAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,KAAW,MAAA,IAAU,WAAW,UAAA,EAAY;AACtE,MAAA,MAAM,IAAIT,kBAAW,gBAAgB,CAAA;AAAA,IACvC;AACA,IAAA,IAAI,IAAA,KAAS,MAAA,KAAc,OAAO,IAAA,KAAS,YAAY,CAAC,IAAA,CAAA;AACtD,MAAA,MAAM,IAAIA,kBAAW,iCAAiC,CAAA;AACxD,IAAA,IAAI,SAAS,MAAA,IAAa,MAAA,CAAO,UAAA,CAAW,IAAA,EAAM,MAAM,CAAA,GAAI,cAAA;AAC1D,MAAA,MAAM,IAAIA,kBAAW,6BAA6B,CAAA;AACpD,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,CAAC,KAAA,CAAM,QAAQ,SAAS,CAAA;AACrD,MAAA,MAAM,IAAIA,kBAAW,4BAA4B,CAAA;AACnD,IAAA,IAAI,IAAA,KAAS,MAAA,IAAa,MAAA,KAAW,MAAA,IAAa,SAAA,KAAc,MAAA;AAC9D,MAAA,MAAM,IAAIA,kBAAW,6BAA6B,CAAA;AACpD,IAAA,MAAM,OAAA,GAAU,IAAA,KAAS,MAAA,IAAa,SAAA,KAAc,MAAA;AACpD,IAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,GAAG,CAAA;AACvC,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AACvE,IAAA,MAAM,iBAAA,GAAoBa,yBAAA,CAAkB,GAAA,EAAK,OAAO,CAAA;AAExD,IAAA,IAAI,GAAA,CAAI,eAAe,IAAA,EAAM;AAE3B,MAAA,IAAI,WAAW,MAAA,KAAW,MAAA,EAAQ,MAAM,IAAIb,kBAAW,+BAA+B,CAAA;AAItF,MAAA,MAAM,mBAAmB,MAAM,uBAAA;AAAA,QAC7BgB,iDAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF;AACA,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAAN,oBAAA,CAAa,MAAA,EAAQ;AAAA,UACnB,IAAA,EAAM,QAAA;AAAA,UACN,EAAA,EAAI,QAAA;AAAA,UACJ,UAAA,EAAY,kBAAA;AAAA,UACZ,aAAA,EAAe;AAAA,SAChB,CAAA;AACD,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,QAAA;AAAA,MACF;AACA,MAAA,iBAAA,CAAkB,SAAS,GAAG,CAAA;AAI9B,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,WAAA,CAAY,OAAO,EAAA,KAAO;AACrD,QAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,EAAE,QAAA,EAAU,EAAA,EAAI,SAAA,EAAW,IAAA,EAAM,CAAA;AACvE,QAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,UAAA,KAAe,IAAA,EAAM;AACvC,UAAA,MAAM,IAAID,qBAAc,mBAAmB,CAAA;AAAA,QAC7C;AACA,QAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,EAAA,EAAI,EAAE,CAAA;AAAA,MACjC,CAAC,CAAA;AACD,MAAAC,oBAAA,CAAa,MAAA,EAAQ;AAAA,QACnB,IAAA,EAAM,UAAA;AAAA,QACN,EAAA,EAAI,SAAA;AAAA,QACJ,SAAS,GAAA,CAAI,QAAA;AAAA,QACb,WAAW,GAAA,CAAI;AAAA,OAChB,CAAA;AACD,MAAA,GAAA,CAAI,IAAA,CAAKG,yBAAA,CAAkB,QAAA,EAAW,OAAO,CAAC,CAAA;AAC9C,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,OAAA,EAAS;AAEX,MAAA,MAAM,mBAAmB,MAAM,uBAAA;AAAA,QAC7BI,+CAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF;AACA,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAAP,oBAAA,CAAa,MAAA,EAAQ;AAAA,UACnB,IAAA,EAAM,QAAA;AAAA,UACN,EAAA,EAAI,QAAA;AAAA,UACJ,UAAA,EAAY,gBAAA;AAAA,UACZ,aAAA,EAAe;AAAA,SAChB,CAAA;AACD,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,QAAA;AAAA,MACF;AACA,MAAA,iBAAA,CAAkB,SAAS,GAAG,CAAA;AAAA,IAChC;AAEA,IAAA,IAAI,WAAW,MAAA,EAAW;AAExB,MAAA,IAAI,IAAI,SAAA,KAAc,IAAA,EAAM,MAAM,IAAIV,kBAAW,wBAAwB,CAAA;AAEzE,MAAA,MAAM,mBAAmB,MAAM,uBAAA;AAAA,QAC7BkB,kDAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF;AACA,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAAR,oBAAA,CAAa,MAAA,EAAQ;AAAA,UACnB,IAAA,EAAM,QAAA;AAAA,UACN,EAAA,EAAI,QAAA;AAAA,UACJ,UAAA,EAAY,mBAAA;AAAA,UACZ,aAAA,EAAe;AAAA,SAChB,CAAA;AACD,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,MAAA,CAAO,IAAI,EAAA,EAAI;AAAA,MACzC,GAAI,IAAA,KAAS,MAAA,GAAY,EAAE,IAAA,KAAS,EAAC;AAAA,MACrC,GAAI,SAAA,KAAc,MAAA,GAAY,EAAE,SAAA,KAAc,EAAC;AAAA,MAC/C,GAAI,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,KAAW,EAAC;AAAA,MACzC,WAAA,EAAa;AAAA,KACd,CAAA;AACD,IAAAA,oBAAA,CAAa,MAAA,EAAQ;AAAA,MACnB,IAAA,EAAM,UAAA;AAAA,MACN,EAAA,EAAI,SAAS,SAAA,GAAY,MAAA;AAAA,MACzB,SAAS,GAAA,CAAI,QAAA;AAAA,MACb,WAAW,GAAA,CAAI;AAAA,KAChB,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAKG,yBAAA,CAAkB,OAAA,EAAU,OAAO,CAAC,CAAA;AAE7C,IAAA,IAAI,WAAW,UAAA,EAAY;AAEzB,MAAA,MAAM,YAAA,GAAe,MAAME,oBAAA,CAAc;AAAA,QACvC,UAAU,IAAA,CAAK,QAAA;AAAA,QACf,MAAM,IAAA,CAAK,IAAA;AAAA,QACX,OAAA;AAAA,QACA;AAAA,OACD,CAAA,CACE,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,aAAA,EAAe,WAAA,IAAe,MAAS,CAAA,CACrD,KAAA,CAAM,MAAM,MAAS,CAAA;AACxB,MAAA,KAAK,IAAA,CAAK,SAAA,EAAW,iBAAA,CAAkB,OAAA,EAAU,SAAS,YAAY,CAAA;AAAA,IACxE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,MAAA,CAAO,eAAA,EAAiB,OAAO,GAAA,EAAK,GAAA,KAAQ;AACjD,IAAA,MAAM,MAAM,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,OAAO,EAAE,CAAA;AACzC,IAAA,IAAI,CAAC,GAAA,IAAO,GAAA,CAAI,SAAA,KAAc,IAAA,IAAQ,IAAI,UAAA,KAAe,IAAA;AACvD,MAAA,MAAM,IAAIN,qBAAc,iBAAiB,CAAA;AAC3C,IAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,GAAG,CAAA;AACvC,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AACvE,IAAA,MAAM,iBAAA,GAAoBI,yBAAA,CAAkB,GAAA,EAAK,OAAO,CAAA;AAExD,IAAA,MAAM,mBAAmB,MAAM,uBAAA;AAAA,MAC7BG,iDAAA;AAAA,MACA,WAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,MAAAN,oBAAA,CAAa,MAAA,EAAQ;AAAA,QACnB,IAAA,EAAM,QAAA;AAAA,QACN,EAAA,EAAI,QAAA;AAAA,QACJ,UAAA,EAAY,kBAAA;AAAA,QACZ,aAAA,EAAe;AAAA,OAChB,CAAA;AACD,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,GAAA,EAAI;AACpB,MAAA;AAAA,IACF;AACA,IAAA,iBAAA,CAAkB,SAAS,GAAG,CAAA;AAM9B,IAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,WAAA,CAAY,OAAO,EAAA,KAAO;AACpD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,EAAE,QAAA,EAAU,EAAA,EAAI,SAAA,EAAW,IAAA,EAAM,CAAA;AACvE,MAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,cAAc,IAAA,IAAQ,KAAA,CAAM,eAAe,IAAA,EAAM;AACnE,QAAA,MAAM,IAAID,qBAAc,iBAAiB,CAAA;AAAA,MAC3C;AACA,MAAA,OAAO,KAAA,CAAM,UAAA,CAAW,GAAA,CAAI,EAAA,EAAI,EAAE,CAAA;AAAA,IACpC,CAAC,CAAA;AACD,IAAAC,oBAAA,CAAa,MAAA,EAAQ;AAAA,MACnB,IAAA,EAAM,UAAA;AAAA,MACN,EAAA,EAAI,QAAA;AAAA,MACJ,SAAS,GAAA,CAAI,QAAA;AAAA,MACb,WAAW,GAAA,CAAI;AAAA,KAChB,CAAA;AAKD,IAAA,GAAA,CAAI,IAAA,CAAKG,yBAAA,CAAkB,OAAA,EAAU,OAAO,CAAC,CAAA;AAAA,EAC/C,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;;"}
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ var luxon = require('luxon');
4
+
5
+ function toIso(value) {
6
+ if (value === null || value === void 0) {
7
+ return void 0;
8
+ }
9
+ let dt;
10
+ if (value instanceof Date || typeof value === "object" && typeof value.getTime === "function") {
11
+ dt = luxon.DateTime.fromJSDate(value);
12
+ } else if (typeof value === "number") {
13
+ dt = luxon.DateTime.fromMillis(value);
14
+ } else if (value.includes(" ")) {
15
+ dt = luxon.DateTime.fromSQL(value, { zone: "utc" });
16
+ } else {
17
+ dt = luxon.DateTime.fromISO(value, { zone: "utc" });
18
+ }
19
+ return dt.toUTC().toISO() ?? void 0;
20
+ }
21
+
22
+ exports.toIso = toIso;
23
+ //# sourceMappingURL=timestamps.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.cjs.js","sources":["../../src/comments/timestamps.ts"],"sourcesContent":["import { DateTime } from \"luxon\";\n\n/**\n * Normalize a dateTime column read across drivers to an RFC3339 UTC string.\n * Returns undefined for null/undefined.\n *\n * Driver shapes:\n * - Postgres → JS Date object\n * - sqlite3 → ISO-like string (\"YYYY-MM-DD HH:MM:SS\" or ISO)\n * - better-sqlite3 → epoch-millis number\n */\nexport function toIso(value: Date | string | number | null | undefined): string | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n let dt: DateTime;\n if (\n value instanceof Date ||\n (typeof value === \"object\" && typeof (value as any).getTime === \"function\")\n ) {\n dt = DateTime.fromJSDate(value as Date);\n } else if (typeof value === \"number\") {\n dt = DateTime.fromMillis(value);\n } else if ((value as string).includes(\" \")) {\n dt = DateTime.fromSQL(value, { zone: \"utc\" }); // \"YYYY-MM-DD HH:MM:SS\"\n } else {\n dt = DateTime.fromISO(value, { zone: \"utc\" });\n }\n return dt.toUTC().toISO() ?? undefined;\n}\n"],"names":["DateTime"],"mappings":";;;;AAWO,SAAS,MAAM,KAAA,EAAsE;AAC1F,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,IAAI,EAAA;AACJ,EAAA,IACE,KAAA,YAAiB,QAChB,OAAO,KAAA,KAAU,YAAY,OAAQ,KAAA,CAAc,YAAY,UAAA,EAChE;AACA,IAAA,EAAA,GAAKA,cAAA,CAAS,WAAW,KAAa,CAAA;AAAA,EACxC,CAAA,MAAA,IAAW,OAAO,KAAA,KAAU,QAAA,EAAU;AACpC,IAAA,EAAA,GAAKA,cAAA,CAAS,WAAW,KAAK,CAAA;AAAA,EAChC,CAAA,MAAA,IAAY,KAAA,CAAiB,QAAA,CAAS,GAAG,CAAA,EAAG;AAC1C,IAAA,EAAA,GAAKA,eAAS,OAAA,CAAQ,KAAA,EAAO,EAAE,IAAA,EAAM,OAAO,CAAA;AAAA,EAC9C,CAAA,MAAO;AACL,IAAA,EAAA,GAAKA,eAAS,OAAA,CAAQ,KAAA,EAAO,EAAE,IAAA,EAAM,OAAO,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,EAAA,CAAG,KAAA,EAAM,CAAE,KAAA,EAAM,IAAK,MAAA;AAC/B;;;;"}
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ function sectionRefOf(pageRef) {
4
+ const i = pageRef.indexOf("#");
5
+ return i === -1 ? pageRef : pageRef.slice(0, i);
6
+ }
7
+ function subpathOf(pageRef) {
8
+ const i = pageRef.indexOf("#");
9
+ return i === -1 ? "" : pageRef.slice(i + 1);
10
+ }
11
+
12
+ exports.sectionRefOf = sectionRefOf;
13
+ exports.subpathOf = subpathOf;
14
+ //# sourceMappingURL=types.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.cjs.js","sources":["../../src/comments/types.ts"],"sourcesContent":["export type CommentStatus = \"open\" | \"resolved\";\n\nexport interface AuthorProfile {\n displayName?: string;\n picture?: string;\n}\n\n/** snake_case DB row. Timestamp columns come back as Date (pg), string (sqlite), or\n * number (better-sqlite3, which stores dateTime as epoch milliseconds). */\nexport interface CommentRow {\n id: string;\n site_ref: string;\n page_ref: string;\n /** the comment's canonical section ref, stored verbatim from page_ref; join key for owner\n * derivation via siteIndex's `sections` table; a routing/filter hint, never an\n * authorization input. */\n section_ref: string;\n parent_id: string | null;\n author_ref: string;\n author_profile: string | null; // JSON AuthorProfile\n body: string;\n body_html: string;\n selectors: string; // JSON unknown[]\n status: CommentStatus;\n created_at: Date | string | number;\n updated_at: Date | string | number;\n resolved_at: Date | string | number | null;\n resolved_by: string | null;\n deleted_at: Date | string | number | null;\n}\n\nexport interface CreateCommentInput {\n pageRef: string;\n parentId?: string;\n authorRef: string;\n authorProfile?: AuthorProfile;\n body: string;\n selectors: unknown[];\n}\n\nexport interface ListFilter {\n pageRef?: string;\n sectionRef?: string;\n status?: CommentStatus;\n parentId?: string | null;\n topLevelOnly?: boolean;\n}\n\n/** The section ref portion of a pageRef (\"<sectionRef>#<subpath>\"), verbatim.\n * rw-core already produced the canonical ref; no transformation/collapse. Total accessor\n * (no-'#' case returns the whole string); the router's `parsePageRef` extracts the same\n * sectionRef prefix for well-formed refs but additionally rejects the no-/leading-'#' cases with\n * a 400 at the HTTP boundary — keep the two in sync if the format changes. */\nexport function sectionRefOf(pageRef: string): string {\n const i = pageRef.indexOf(\"#\");\n return i === -1 ? pageRef : pageRef.slice(0, i);\n}\n\n/** The subpath portion of a pageRef (\"<sectionRef>#<subpath>\").\n * Returns empty string when there is no \"#\". */\nexport function subpathOf(pageRef: string): string {\n const i = pageRef.indexOf(\"#\");\n return i === -1 ? \"\" : pageRef.slice(i + 1);\n}\n"],"names":[],"mappings":";;AAqDO,SAAS,aAAa,OAAA,EAAyB;AACpD,EAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC7B,EAAA,OAAO,MAAM,EAAA,GAAK,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAC,CAAA;AAChD;AAIO,SAAS,UAAU,OAAA,EAAyB;AACjD,EAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC7B,EAAA,OAAO,MAAM,EAAA,GAAK,EAAA,GAAK,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAC,CAAA;AAC5C;;;;;"}
package/dist/hub.cjs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@rwdocs/core');
4
- var entityPath = require('./entityPath.cjs.js');
4
+ var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
5
5
 
6
6
  class Hub {
7
7
  options;
@@ -10,7 +10,7 @@ class Hub {
10
10
  constructor(options) {
11
11
  this.options = {
12
12
  ...options,
13
- entity: options.entity ? entityPath.toEntityPath(options.entity) : void 0
13
+ entity: options.entity ? backstagePluginRwCommon.toEntityPath(options.entity) : void 0
14
14
  };
15
15
  this.maxSize = options.cacheSize ?? 20;
16
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hub.cjs.js","sources":["../src/hub.ts"],"sourcesContent":["import type { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { createSite, type RwSite, type SiteConfig, type DiagramsConfig } from \"@rwdocs/core\";\nimport { toEntityPath } from \"./entityPath\";\n\nexport interface S3SharedConfig {\n bucket: string;\n region?: string;\n endpoint?: string;\n bucketRootPath?: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n}\n\nexport interface HubOptions {\n s3?: S3SharedConfig;\n projectDir?: string;\n /** Entity ref in any format accepted by parseEntityRef. Normalized internally. */\n entity?: string;\n diagrams?: DiagramsConfig;\n cacheSize?: number;\n}\n\nexport class Hub {\n private readonly options: HubOptions;\n private readonly cache: Map<string, RwSite> = new Map();\n private readonly maxSize: number;\n\n constructor(options: HubOptions) {\n this.options = {\n ...options,\n entity: options.entity ? toEntityPath(options.entity) : undefined,\n };\n this.maxSize = options.cacheSize ?? 20;\n }\n\n getSite(entityRef: string): RwSite | undefined {\n if (this.options.projectDir) {\n return this.getLocalSite(entityRef);\n }\n return this.getS3Site(entityRef);\n }\n\n private getLocalSite(entityRef: string): RwSite | undefined {\n if (entityRef !== this.options.entity) {\n return undefined;\n }\n\n const cached = this.cache.get(entityRef);\n if (cached) return cached;\n\n const site = createSite({\n projectDir: this.options.projectDir,\n diagrams: this.options.diagrams,\n });\n this.cache.set(entityRef, site);\n return site;\n }\n\n async reloadAll(logger: LoggerService) {\n const entries = [...this.cache.entries()];\n for (const [ref, site] of entries) {\n try {\n const reloaded = await site.reload();\n if (reloaded) {\n logger.info(`Reloaded site: ${ref}`);\n }\n } catch (err) {\n logger.warn(`Failed to reload site ${ref}: ${err}`);\n }\n }\n }\n\n private getS3Site(entityRef: string): RwSite {\n const cached = this.cache.get(entityRef);\n if (cached) {\n this.cache.delete(entityRef);\n this.cache.set(entityRef, cached);\n return cached;\n }\n\n if (this.cache.size >= this.maxSize) {\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n\n const s3 = this.options.s3!;\n const config: SiteConfig = {\n s3: { ...s3, entity: entityRef },\n diagrams: this.options.diagrams,\n };\n\n const site = createSite(config);\n this.cache.set(entityRef, site);\n return site;\n }\n}\n"],"names":["toEntityPath","createSite"],"mappings":"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"}
1
+ {"version":3,"file":"hub.cjs.js","sources":["../src/hub.ts"],"sourcesContent":["import type { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { createSite, type RwSite, type SiteConfig, type DiagramsConfig } from \"@rwdocs/core\";\nimport { toEntityPath, type S3Config } from \"@rwdocs/backstage-plugin-rw-common\";\n\nexport interface HubOptions {\n s3?: S3Config;\n projectDir?: string;\n /** Entity ref in any format accepted by parseEntityRef. Normalized internally. */\n entity?: string;\n diagrams?: DiagramsConfig;\n cacheSize?: number;\n}\n\nexport class Hub {\n private readonly options: HubOptions;\n private readonly cache: Map<string, RwSite> = new Map();\n private readonly maxSize: number;\n\n constructor(options: HubOptions) {\n this.options = {\n ...options,\n entity: options.entity ? toEntityPath(options.entity) : undefined,\n };\n this.maxSize = options.cacheSize ?? 20;\n }\n\n getSite(entityRef: string): RwSite | undefined {\n if (this.options.projectDir) {\n return this.getLocalSite(entityRef);\n }\n return this.getS3Site(entityRef);\n }\n\n private getLocalSite(entityRef: string): RwSite | undefined {\n if (entityRef !== this.options.entity) {\n return undefined;\n }\n\n const cached = this.cache.get(entityRef);\n if (cached) return cached;\n\n const site = createSite({\n projectDir: this.options.projectDir,\n diagrams: this.options.diagrams,\n });\n this.cache.set(entityRef, site);\n return site;\n }\n\n async reloadAll(logger: LoggerService) {\n const entries = [...this.cache.entries()];\n for (const [ref, site] of entries) {\n try {\n const reloaded = await site.reload();\n if (reloaded) {\n logger.info(`Reloaded site: ${ref}`);\n }\n } catch (err) {\n logger.warn(`Failed to reload site ${ref}: ${err}`);\n }\n }\n }\n\n private getS3Site(entityRef: string): RwSite {\n const cached = this.cache.get(entityRef);\n if (cached) {\n this.cache.delete(entityRef);\n this.cache.set(entityRef, cached);\n return cached;\n }\n\n if (this.cache.size >= this.maxSize) {\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n\n const s3 = this.options.s3!;\n const config: SiteConfig = {\n s3: { ...s3, entity: entityRef },\n diagrams: this.options.diagrams,\n };\n\n const site = createSite(config);\n this.cache.set(entityRef, site);\n return site;\n }\n}\n"],"names":["toEntityPath","createSite"],"mappings":";;;;;AAaO,MAAM,GAAA,CAAI;AAAA,EACE,OAAA;AAAA,EACA,KAAA,uBAAiC,GAAA,EAAI;AAAA,EACrC,OAAA;AAAA,EAEjB,YAAY,OAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,OAAA,GAAU;AAAA,MACb,GAAG,OAAA;AAAA,MACH,QAAQ,OAAA,CAAQ,MAAA,GAASA,oCAAA,CAAa,OAAA,CAAQ,MAAM,CAAA,GAAI;AAAA,KAC1D;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,SAAA,IAAa,EAAA;AAAA,EACtC;AAAA,EAEA,QAAQ,SAAA,EAAuC;AAC7C,IAAA,IAAI,IAAA,CAAK,QAAQ,UAAA,EAAY;AAC3B,MAAA,OAAO,IAAA,CAAK,aAAa,SAAS,CAAA;AAAA,IACpC;AACA,IAAA,OAAO,IAAA,CAAK,UAAU,SAAS,CAAA;AAAA,EACjC;AAAA,EAEQ,aAAa,SAAA,EAAuC;AAC1D,IAAA,IAAI,SAAA,KAAc,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ;AACrC,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AACvC,IAAA,IAAI,QAAQ,OAAO,MAAA;AAEnB,IAAA,MAAM,OAAOC,eAAA,CAAW;AAAA,MACtB,UAAA,EAAY,KAAK,OAAA,CAAQ,UAAA;AAAA,MACzB,QAAA,EAAU,KAAK,OAAA,CAAQ;AAAA,KACxB,CAAA;AACD,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAA,EAAW,IAAI,CAAA;AAC9B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,MAAA,EAAuB;AACrC,IAAA,MAAM,UAAU,CAAC,GAAG,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AACxC,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,IAAI,CAAA,IAAK,OAAA,EAAS;AACjC,MAAA,IAAI;AACF,QAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,MAAA,EAAO;AACnC,QAAA,IAAI,QAAA,EAAU;AACZ,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,eAAA,EAAkB,GAAG,CAAA,CAAE,CAAA;AAAA,QACrC;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,MAAA,CAAO,IAAA,CAAK,CAAA,sBAAA,EAAyB,GAAG,CAAA,EAAA,EAAK,GAAG,CAAA,CAAE,CAAA;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,UAAU,SAAA,EAA2B;AAC3C,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AACvC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,SAAS,CAAA;AAC3B,MAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAA,EAAW,MAAM,CAAA;AAChC,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,IAAQ,IAAA,CAAK,OAAA,EAAS;AACnC,MAAA,MAAM,WAAW,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AAC1C,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,QAAQ,CAAA;AAAA,IAC5B;AAEA,IAAA,MAAM,EAAA,GAAK,KAAK,OAAA,CAAQ,EAAA;AACxB,IAAA,MAAM,MAAA,GAAqB;AAAA,MACzB,EAAA,EAAI,EAAE,GAAG,EAAA,EAAI,QAAQ,SAAA,EAAU;AAAA,MAC/B,QAAA,EAAU,KAAK,OAAA,CAAQ;AAAA,KACzB;AAEA,IAAA,MAAM,IAAA,GAAOA,gBAAW,MAAM,CAAA;AAC9B,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAA,EAAW,IAAI,CAAA;AAC9B,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;;"}
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ var types = require('../comments/types.cjs.js');
4
+
5
+ class InboxStore {
6
+ constructor(knex) {
7
+ this.knex = knex;
8
+ }
9
+ static titleKeyFor(siteRef, sectionRef, subpath) {
10
+ return `${siteRef}\0${sectionRef}\0${subpath}`;
11
+ }
12
+ async titlesFor(keys) {
13
+ const map = /* @__PURE__ */ new Map();
14
+ if (!keys.length) return map;
15
+ const CHUNK = 300;
16
+ for (let i = 0; i < keys.length; i += CHUNK) {
17
+ const chunk = keys.slice(i, i + CHUNK);
18
+ const rows = await this.knex("pages").where((qb) => {
19
+ for (const k of chunk) {
20
+ qb.orWhere(
21
+ (w) => w.where("site_ref", k.site_ref).andWhere("section_ref", k.section_ref).andWhere("subpath", k.subpath)
22
+ );
23
+ }
24
+ }).select("site_ref", "section_ref", "subpath", "title");
25
+ for (const r of rows) {
26
+ map.set(InboxStore.titleKeyFor(r.site_ref, r.section_ref, r.subpath), r.title);
27
+ }
28
+ }
29
+ return map;
30
+ }
31
+ /** Resolves page_title for each raw row via titlesFor, returning OwnedThreadRow[]. */
32
+ async resolveRows(rows) {
33
+ const keys = rows.map((r) => ({
34
+ site_ref: r.site_ref,
35
+ section_ref: types.sectionRefOf(r.page_ref),
36
+ subpath: types.subpathOf(r.page_ref)
37
+ }));
38
+ const titles = await this.titlesFor(keys);
39
+ return rows.map((r, j) => {
40
+ const k = keys[j];
41
+ return {
42
+ ...r,
43
+ // already carries entity_ref + section_path (typed onto the row)
44
+ page_title: titles.get(InboxStore.titleKeyFor(r.site_ref, k.section_ref, k.subpath)) ?? null
45
+ };
46
+ });
47
+ }
48
+ // Open top-level, non-deleted threads owned by `ownerRefs`. Owners are a user's
49
+ // own + group refs (few), so a single whereIn is safe — no chunking.
50
+ baseOwnedQuery(ownerRefs) {
51
+ return this.knex({ c: "comments" }).join({ s: "sections" }, function joinSections() {
52
+ this.on("s.site_ref", "c.site_ref").andOn("s.section_ref", "c.section_ref");
53
+ }).whereIn("s.entity_owner_ref", ownerRefs).andWhere("c.status", "open").whereNull("c.parent_id").whereNull("c.deleted_at");
54
+ }
55
+ // Shared reply-existence predicate: an open, non-deleted reply to the thread.
56
+ // Do NOT change this predicate (open + non-deleted) without making the same
57
+ // change in CommentStore.replyCountsFor: a mismatch will hide threads the rail
58
+ // still marks "No replies yet" (or vice versa — show threads the rail marks
59
+ // answered), breaking the unanswered filter's accuracy.
60
+ whereHasNoOpenReply(q) {
61
+ const knex = this.knex;
62
+ return q.whereNotExists(function noReply() {
63
+ this.select(knex.raw("1")).from({ r: "comments" }).whereRaw("r.parent_id = c.id").andWhere("r.status", "open").whereNull("r.deleted_at");
64
+ });
65
+ }
66
+ async ownedOpenThreadsPage(ownerRefs, params) {
67
+ if (!ownerRefs.length) return { rows: [], hasMore: false };
68
+ const dir = params.sort === "oldest" ? "asc" : "desc";
69
+ const op = params.sort === "oldest" ? ">" : "<";
70
+ let q = this.baseOwnedQuery(ownerRefs);
71
+ if (params.filter === "unanswered") q = this.whereHasNoOpenReply(q);
72
+ if (params.lastKey) {
73
+ const [uv, idv] = params.lastKey;
74
+ q = q.andWhere(function seek() {
75
+ this.where("c.updated_at", op, uv).orWhere(function tie() {
76
+ this.where("c.updated_at", uv).andWhere("c.id", op, idv);
77
+ });
78
+ });
79
+ }
80
+ const raw = await q.orderBy("c.updated_at", dir).orderBy("c.id", dir).select("c.*", "s.entity_ref", "s.section_path").limit(params.limit + 1);
81
+ const hasMore = raw.length > params.limit;
82
+ if (hasMore) raw.length = params.limit;
83
+ return { rows: await this.resolveRows(raw), hasMore };
84
+ }
85
+ async counts(ownerRefs) {
86
+ if (!ownerRefs.length) return { openCount: 0, unansweredCount: 0 };
87
+ const [[openRow], [unansweredRow]] = await Promise.all([
88
+ this.baseOwnedQuery(ownerRefs).count({ cnt: "c.id" }),
89
+ this.whereHasNoOpenReply(this.baseOwnedQuery(ownerRefs)).count({ cnt: "c.id" })
90
+ ]);
91
+ return { openCount: Number(openRow.cnt), unansweredCount: Number(unansweredRow.cnt) };
92
+ }
93
+ }
94
+
95
+ exports.InboxStore = InboxStore;
96
+ //# sourceMappingURL=InboxStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxStore.cjs.js","sources":["../../src/inbox/InboxStore.ts"],"sourcesContent":["import type { Knex } from \"knex\";\nimport type { CommentRow } from \"../comments/types\";\nimport { sectionRefOf, subpathOf } from \"../comments/types\";\n\nexport type OwnedThreadRow = CommentRow & {\n entity_ref: string;\n section_path: string;\n page_title: string | null;\n};\n\nexport interface InboxPageParams {\n filter: \"open\" | \"unanswered\";\n sort: \"newest\" | \"oldest\";\n lastKey?: [string | number, string];\n limit: number;\n}\n\nexport interface InboxPage {\n rows: OwnedThreadRow[];\n hasMore: boolean;\n}\n\nexport class InboxStore {\n constructor(private readonly knex: Knex) {}\n\n private static titleKeyFor(siteRef: string, sectionRef: string, subpath: string): string {\n return `${siteRef}\\0${sectionRef}\\0${subpath}`;\n }\n\n private async titlesFor(\n keys: { site_ref: string; section_ref: string; subpath: string }[],\n ): Promise<Map<string, string>> {\n const map = new Map<string, string>();\n if (!keys.length) return map;\n const CHUNK = 300; // 3 params × 300 = 900 < SQLite's 999-variable limit\n for (let i = 0; i < keys.length; i += CHUNK) {\n const chunk = keys.slice(i, i + CHUNK);\n const rows: Array<{ site_ref: string; section_ref: string; subpath: string; title: string }> =\n await this.knex(\"pages\")\n .where((qb) => {\n for (const k of chunk) {\n qb.orWhere((w) =>\n w\n .where(\"site_ref\", k.site_ref)\n .andWhere(\"section_ref\", k.section_ref)\n .andWhere(\"subpath\", k.subpath),\n );\n }\n })\n .select(\"site_ref\", \"section_ref\", \"subpath\", \"title\");\n for (const r of rows) {\n map.set(InboxStore.titleKeyFor(r.site_ref, r.section_ref, r.subpath), r.title);\n }\n }\n return map;\n }\n\n /** Resolves page_title for each raw row via titlesFor, returning OwnedThreadRow[]. */\n private async resolveRows(\n rows: Array<CommentRow & { entity_ref: string; section_path: string }>,\n ): Promise<OwnedThreadRow[]> {\n const keys = rows.map((r) => ({\n site_ref: r.site_ref,\n section_ref: sectionRefOf(r.page_ref),\n subpath: subpathOf(r.page_ref),\n }));\n const titles = await this.titlesFor(keys);\n return rows.map((r, j) => {\n const k = keys[j];\n return {\n ...r, // already carries entity_ref + section_path (typed onto the row)\n page_title:\n titles.get(InboxStore.titleKeyFor(r.site_ref, k.section_ref, k.subpath)) ?? null,\n };\n });\n }\n\n // Open top-level, non-deleted threads owned by `ownerRefs`. Owners are a user's\n // own + group refs (few), so a single whereIn is safe — no chunking.\n private baseOwnedQuery(ownerRefs: string[]) {\n return this.knex({ c: \"comments\" })\n .join({ s: \"sections\" }, function joinSections(this: Knex.JoinClause) {\n this.on(\"s.site_ref\", \"c.site_ref\").andOn(\"s.section_ref\", \"c.section_ref\");\n })\n .whereIn(\"s.entity_owner_ref\", ownerRefs)\n .andWhere(\"c.status\", \"open\")\n .whereNull(\"c.parent_id\")\n .whereNull(\"c.deleted_at\");\n }\n\n // Shared reply-existence predicate: an open, non-deleted reply to the thread.\n // Do NOT change this predicate (open + non-deleted) without making the same\n // change in CommentStore.replyCountsFor: a mismatch will hide threads the rail\n // still marks \"No replies yet\" (or vice versa — show threads the rail marks\n // answered), breaking the unanswered filter's accuracy.\n private whereHasNoOpenReply(q: Knex.QueryBuilder) {\n const knex = this.knex;\n return q.whereNotExists(function noReply(this: Knex.QueryBuilder) {\n this.select(knex.raw(\"1\"))\n .from({ r: \"comments\" })\n .whereRaw(\"r.parent_id = c.id\")\n .andWhere(\"r.status\", \"open\")\n .whereNull(\"r.deleted_at\");\n });\n }\n\n async ownedOpenThreadsPage(ownerRefs: string[], params: InboxPageParams): Promise<InboxPage> {\n if (!ownerRefs.length) return { rows: [], hasMore: false };\n const dir = params.sort === \"oldest\" ? \"asc\" : \"desc\";\n const op = params.sort === \"oldest\" ? \">\" : \"<\";\n\n let q = this.baseOwnedQuery(ownerRefs);\n if (params.filter === \"unanswered\") q = this.whereHasNoOpenReply(q);\n if (params.lastKey) {\n const [uv, idv] = params.lastKey;\n q = q.andWhere(function seek(this: Knex.QueryBuilder) {\n this.where(\"c.updated_at\", op, uv).orWhere(function tie(this: Knex.QueryBuilder) {\n this.where(\"c.updated_at\", uv).andWhere(\"c.id\", op, idv);\n });\n });\n }\n\n const raw: Array<CommentRow & { entity_ref: string; section_path: string }> = await q\n .orderBy(\"c.updated_at\", dir)\n .orderBy(\"c.id\", dir)\n .select(\"c.*\", \"s.entity_ref\", \"s.section_path\")\n .limit(params.limit + 1);\n\n const hasMore = raw.length > params.limit;\n if (hasMore) raw.length = params.limit;\n return { rows: await this.resolveRows(raw), hasMore };\n }\n\n async counts(ownerRefs: string[]): Promise<{ openCount: number; unansweredCount: number }> {\n if (!ownerRefs.length) return { openCount: 0, unansweredCount: 0 };\n // The two aggregates are independent — run them concurrently.\n const [[openRow], [unansweredRow]] = await Promise.all([\n this.baseOwnedQuery(ownerRefs).count({ cnt: \"c.id\" }),\n this.whereHasNoOpenReply(this.baseOwnedQuery(ownerRefs)).count({ cnt: \"c.id\" }),\n ]);\n return { openCount: Number(openRow.cnt), unansweredCount: Number(unansweredRow.cnt) };\n }\n}\n"],"names":["sectionRefOf","subpathOf"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var errors = require('@backstage/errors');
5
+
6
+ const cursorSchema = zod.z.object({
7
+ filter: zod.z.enum(["open", "unanswered"]),
8
+ sort: zod.z.enum(["newest", "oldest"]),
9
+ // [raw updated_at column value, comment id]. updated_at is the column's native
10
+ // value per driver (number for better-sqlite3, string for sqlite3/pg ISO), kept
11
+ // opaque so it round-trips back into the seek against its own column type.
12
+ lastKey: zod.z.tuple([zod.z.union([zod.z.string(), zod.z.number()]), zod.z.string()]),
13
+ openCount: zod.z.number().int().nonnegative(),
14
+ unansweredCount: zod.z.number().int().nonnegative()
15
+ });
16
+ function encodeCursor(cursor) {
17
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64");
18
+ }
19
+ function decodeCursor(encoded) {
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
23
+ } catch {
24
+ throw new errors.InputError("Malformed inbox cursor");
25
+ }
26
+ const result = cursorSchema.safeParse(parsed);
27
+ if (!result.success) {
28
+ throw new errors.InputError(`Malformed inbox cursor: ${result.error.message}`);
29
+ }
30
+ return result.data;
31
+ }
32
+
33
+ exports.decodeCursor = decodeCursor;
34
+ exports.encodeCursor = encodeCursor;
35
+ //# sourceMappingURL=cursor.cjs.js.map