@igstack/app-catalog-backend-core 6.3.0 → 6.4.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.
Files changed (106) hide show
  1. package/dist/db/syncAppCatalog.mjs +1 -0
  2. package/dist/db/syncAppCatalog.mjs.map +1 -1
  3. package/dist/generated/prisma/client.mjs.map +1 -1
  4. package/dist/generated/prisma/commonInputTypes.d.mts +72 -72
  5. package/dist/generated/prisma/commonInputTypes.d.mts.map +1 -1
  6. package/dist/generated/prisma/internal/class.d.mts +17 -5
  7. package/dist/generated/prisma/internal/class.d.mts.map +1 -1
  8. package/dist/generated/prisma/internal/class.mjs +4 -4
  9. package/dist/generated/prisma/internal/class.mjs.map +1 -1
  10. package/dist/generated/prisma/internal/prismaNamespace.d.mts +145 -51
  11. package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
  12. package/dist/generated/prisma/models/DbApprovalMethod.d.mts +30 -3
  13. package/dist/generated/prisma/models/DbApprovalMethod.d.mts.map +1 -1
  14. package/dist/generated/prisma/models/DbAsset.d.mts +2 -11
  15. package/dist/generated/prisma/models/DbAsset.d.mts.map +1 -1
  16. package/dist/generated/prisma/models/DbFeedback.d.mts +1565 -0
  17. package/dist/generated/prisma/models/DbFeedback.d.mts.map +1 -0
  18. package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts +1431 -0
  19. package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts.map +1 -0
  20. package/dist/generated/prisma/models/DbResource.d.mts +108 -60
  21. package/dist/generated/prisma/models/DbResource.d.mts.map +1 -1
  22. package/dist/generated/prisma/models/SourceReference.d.mts +1 -1
  23. package/dist/generated/prisma/models.d.mts +4 -3
  24. package/dist/index.d.mts +5 -3
  25. package/dist/index.mjs +5 -3
  26. package/dist/middleware/createAcMiddleware.mjs +2 -2
  27. package/dist/middleware/createAcMiddleware.mjs.map +1 -1
  28. package/dist/middleware/featureRegistry.mjs +3 -1
  29. package/dist/middleware/featureRegistry.mjs.map +1 -1
  30. package/dist/modules/appCatalog/service.d.mts.map +1 -1
  31. package/dist/modules/appCatalog/service.mjs +1 -0
  32. package/dist/modules/appCatalog/service.mjs.map +1 -1
  33. package/dist/modules/feedback/feedbackAttachmentController.d.mts +22 -0
  34. package/dist/modules/feedback/feedbackAttachmentController.d.mts.map +1 -0
  35. package/dist/modules/feedback/feedbackAttachmentController.mjs +192 -0
  36. package/dist/modules/feedback/feedbackAttachmentController.mjs.map +1 -0
  37. package/dist/modules/feedback/feedbackRouter.d.mts +54 -0
  38. package/dist/modules/feedback/feedbackRouter.d.mts.map +1 -0
  39. package/dist/modules/feedback/feedbackRouter.mjs +59 -0
  40. package/dist/modules/feedback/feedbackRouter.mjs.map +1 -0
  41. package/dist/modules/feedback/service.d.mts +42 -0
  42. package/dist/modules/feedback/service.d.mts.map +1 -0
  43. package/dist/modules/feedback/service.mjs +165 -0
  44. package/dist/modules/feedback/service.mjs.map +1 -0
  45. package/dist/modules/visitor/visitorIdentity.d.mts +86 -0
  46. package/dist/modules/visitor/visitorIdentity.d.mts.map +1 -0
  47. package/dist/modules/{comments → visitor}/visitorIdentity.mjs +53 -2
  48. package/dist/modules/visitor/visitorIdentity.mjs.map +1 -0
  49. package/dist/server/acTrpcContext.d.mts +8 -7
  50. package/dist/server/acTrpcContext.d.mts.map +1 -1
  51. package/dist/server/acTrpcContext.mjs +2 -2
  52. package/dist/server/acTrpcContext.mjs.map +1 -1
  53. package/dist/server/controller.d.mts +15 -8
  54. package/dist/server/controller.d.mts.map +1 -1
  55. package/dist/server/controller.mjs +2 -2
  56. package/dist/server/controller.mjs.map +1 -1
  57. package/dist/types/common/appCatalogTypes.d.mts +12 -0
  58. package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
  59. package/dist/types/common/approvalMethodTypes.d.mts +12 -0
  60. package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
  61. package/package.json +5 -5
  62. package/prisma/migrations/20261004000000_feedback_replaces_comments/migration.sql +74 -0
  63. package/prisma/schema.prisma +84 -24
  64. package/src/__tests__/feedbackAttachmentUpload.test.ts +192 -0
  65. package/src/__tests__/{comments.test.ts → visitorIdentity.test.ts} +1 -1
  66. package/src/db/syncAppCatalog.ts +1 -0
  67. package/src/generated/prisma/browser.ts +22 -7
  68. package/src/generated/prisma/client.ts +22 -7
  69. package/src/generated/prisma/commonInputTypes.ts +85 -85
  70. package/src/generated/prisma/internal/class.ts +18 -8
  71. package/src/generated/prisma/internal/prismaNamespace.ts +143 -47
  72. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +24 -3
  73. package/src/generated/prisma/models/DbApprovalMethod.ts +29 -1
  74. package/src/generated/prisma/models/DbAsset.ts +0 -12
  75. package/src/generated/prisma/models/DbFeedback.ts +1814 -0
  76. package/src/generated/prisma/models/DbFeedbackAttachment.ts +1661 -0
  77. package/src/generated/prisma/models/DbResource.ts +107 -58
  78. package/src/generated/prisma/models.ts +2 -1
  79. package/src/index.ts +24 -4
  80. package/src/middleware/createAcMiddleware.ts +4 -3
  81. package/src/middleware/featureRegistry.ts +7 -0
  82. package/src/modules/appCatalog/service.ts +1 -0
  83. package/src/modules/feedback/feedbackAttachmentController.ts +248 -0
  84. package/src/modules/feedback/feedbackRouter.ts +102 -0
  85. package/src/modules/feedback/service.ts +295 -0
  86. package/src/modules/{comments → visitor}/visitorIdentity.ts +84 -0
  87. package/src/server/acTrpcContext.ts +9 -8
  88. package/src/server/controller.ts +2 -2
  89. package/src/types/common/appCatalogTypes.ts +12 -0
  90. package/src/types/common/approvalMethodTypes.ts +12 -0
  91. package/dist/generated/prisma/models/DbComment.d.mts +0 -1340
  92. package/dist/generated/prisma/models/DbComment.d.mts.map +0 -1
  93. package/dist/modules/comments/commentsRouter.d.mts +0 -47
  94. package/dist/modules/comments/commentsRouter.d.mts.map +0 -1
  95. package/dist/modules/comments/commentsRouter.mjs +0 -55
  96. package/dist/modules/comments/commentsRouter.mjs.map +0 -1
  97. package/dist/modules/comments/service.d.mts +0 -40
  98. package/dist/modules/comments/service.d.mts.map +0 -1
  99. package/dist/modules/comments/service.mjs +0 -88
  100. package/dist/modules/comments/service.mjs.map +0 -1
  101. package/dist/modules/comments/visitorIdentity.d.mts +0 -15
  102. package/dist/modules/comments/visitorIdentity.d.mts.map +0 -1
  103. package/dist/modules/comments/visitorIdentity.mjs.map +0 -1
  104. package/src/generated/prisma/models/DbComment.ts +0 -1561
  105. package/src/modules/comments/commentsRouter.ts +0 -85
  106. package/src/modules/comments/service.ts +0 -174
@@ -0,0 +1,165 @@
1
+ import { getDbClient } from "../../db/client.mjs";
2
+
3
+ //#region src/modules/feedback/service.ts
4
+ /** How long an author may edit their own wording. */
5
+ const EDIT_WINDOW_MS = 3600 * 1e3;
6
+ const MAX_BODY_LENGTH = 1e3;
7
+ const MAX_SUBJECT_LENGTH = 120;
8
+ const MAX_ATTACHMENTS = 3;
9
+ function toView(row, actor) {
10
+ const isMine = !!actor && row.authorHash === actor.hash;
11
+ return {
12
+ id: row.id,
13
+ authorName: row.authorAlias,
14
+ body: row.body,
15
+ subject: row.subject,
16
+ resourceSlug: row.resource?.slug ?? null,
17
+ resourceName: row.resource?.displayName ?? null,
18
+ attachmentIds: (row.attachments ?? []).map((a) => a.id),
19
+ status: row.status,
20
+ reviewerReply: row.reviewerReply,
21
+ reviewedAt: row.reviewedAt?.toISOString() ?? null,
22
+ createdAt: row.createdAt.toISOString(),
23
+ editedAt: row.editedAt?.toISOString() ?? null,
24
+ isMine,
25
+ canEditUntil: isMine ? new Date(row.createdAt.getTime() + EDIT_WINDOW_MS).toISOString() : null
26
+ };
27
+ }
28
+ const WITH_RELATIONS = {
29
+ resource: { select: {
30
+ slug: true,
31
+ displayName: true
32
+ } },
33
+ attachments: { select: { id: true } }
34
+ };
35
+ async function resourceIdForSlug(slug) {
36
+ return (await getDbClient().dbResource.findUnique({
37
+ where: { slug },
38
+ select: { id: true }
39
+ }))?.id ?? null;
40
+ }
41
+ /** Oldest first: a thread reads top to bottom. */
42
+ async function listFeedback(resourceSlug, actor) {
43
+ const resourceId = await resourceIdForSlug(resourceSlug);
44
+ if (!resourceId) return {
45
+ items: [],
46
+ openCount: 0
47
+ };
48
+ const rows = await getDbClient().dbFeedback.findMany({
49
+ where: { resourceId },
50
+ orderBy: { createdAt: "asc" },
51
+ include: WITH_RELATIONS
52
+ });
53
+ return {
54
+ items: rows.map((row) => toView(row, actor)),
55
+ openCount: rows.filter((row) => row.status === null).length
56
+ };
57
+ }
58
+ /**
59
+ * Everything this browser filed, across entries, for the tracker.
60
+ *
61
+ * Sorted the way the tracker renders: unreviewed first, then newest. Postgres sorts
62
+ * NULLs last by default, so `status` ascending would bury the pending rows — hence the
63
+ * explicit two-key ordering rather than relying on the default.
64
+ */
65
+ async function listMyFeedback(actor) {
66
+ if (!actor) return [];
67
+ return (await getDbClient().dbFeedback.findMany({
68
+ where: { authorHash: actor.hash },
69
+ orderBy: [{ status: {
70
+ sort: "asc",
71
+ nulls: "first"
72
+ } }, { createdAt: "desc" }],
73
+ include: WITH_RELATIONS
74
+ })).map((row) => toView(row, actor));
75
+ }
76
+ var FeedbackError = class extends Error {
77
+ constructor(reason, message) {
78
+ super(message);
79
+ this.reason = reason;
80
+ }
81
+ };
82
+ function normaliseBody(body) {
83
+ if (body === void 0 || body === null) return null;
84
+ const trimmed = body.trim();
85
+ return trimmed ? trimmed.slice(0, MAX_BODY_LENGTH) : null;
86
+ }
87
+ async function addFeedback(input, actor) {
88
+ const { resourceSlug, subject, body, attachmentIds = [] } = input;
89
+ if (!resourceSlug && !subject?.trim()) throw new FeedbackError("empty", "Tell us which entry this is about, or what you were looking for");
90
+ if (attachmentIds.length > 3) throw new FeedbackError("too-many-attachments", `Up to ${3} images`);
91
+ let resourceId = null;
92
+ if (resourceSlug) {
93
+ resourceId = await resourceIdForSlug(resourceSlug);
94
+ if (!resourceId) throw new FeedbackError("not-found", `Unknown resource: ${resourceSlug}`);
95
+ }
96
+ return toView(await getDbClient().$transaction(async (tx) => {
97
+ const created = await tx.dbFeedback.create({
98
+ data: {
99
+ resourceId,
100
+ subject: resourceId ? null : subject.trim().slice(0, 120),
101
+ authorHash: actor.hash,
102
+ authorAlias: actor.alias,
103
+ body: normaliseBody(body)
104
+ },
105
+ select: { id: true }
106
+ });
107
+ if (attachmentIds.length > 0) await tx.dbFeedbackAttachment.updateMany({
108
+ where: {
109
+ id: { in: attachmentIds },
110
+ authorHash: actor.hash,
111
+ feedbackId: null
112
+ },
113
+ data: { feedbackId: created.id }
114
+ });
115
+ return tx.dbFeedback.findUniqueOrThrow({
116
+ where: { id: created.id },
117
+ include: WITH_RELATIONS
118
+ });
119
+ }), actor);
120
+ }
121
+ /**
122
+ * Load a row and check this actor may still change its wording.
123
+ *
124
+ * Enforced here, not in the UI: the control disappearing is a courtesy, and a request
125
+ * that arrives a second late must still be refused.
126
+ */
127
+ async function loadOwnForEdit(id, actor) {
128
+ const row = await getDbClient().dbFeedback.findUnique({ where: { id } });
129
+ if (!row) throw new FeedbackError("not-found", "This no longer exists");
130
+ if (row.authorHash !== actor.hash) throw new FeedbackError("not-author", "Only the author can change this");
131
+ if (Date.now() > row.createdAt.getTime() + 36e5) throw new FeedbackError("window-closed", "Wording can only be changed in the first hour");
132
+ return row;
133
+ }
134
+ async function editFeedback(id, body, actor) {
135
+ await loadOwnForEdit(id, actor);
136
+ return toView(await getDbClient().dbFeedback.update({
137
+ where: { id },
138
+ data: {
139
+ body: normaliseBody(body),
140
+ editedAt: /* @__PURE__ */ new Date()
141
+ },
142
+ include: WITH_RELATIONS
143
+ }), actor);
144
+ }
145
+ /**
146
+ * Withdraw your own feedback.
147
+ *
148
+ * Allowed for as long as nobody has reviewed it, with no time limit — the edit window
149
+ * exists to stop someone rewriting words others have already read, and that reasoning
150
+ * does not apply to withdrawing something no maintainer has acted on. Once reviewed it
151
+ * is public record carrying a maintainer's reply, so withdrawing would delete their
152
+ * answer; from then on it is refused.
153
+ */
154
+ async function dismissFeedback(id, actor) {
155
+ const row = await getDbClient().dbFeedback.findUnique({ where: { id } });
156
+ if (!row) throw new FeedbackError("not-found", "This no longer exists");
157
+ if (row.authorHash !== actor.hash) throw new FeedbackError("not-author", "Only the author can withdraw this");
158
+ if (row.status !== null) throw new FeedbackError("already-reviewed", "This has been answered, so it stays on the entry");
159
+ await getDbClient().dbFeedback.delete({ where: { id } });
160
+ return { id };
161
+ }
162
+
163
+ //#endregion
164
+ export { EDIT_WINDOW_MS, FeedbackError, MAX_ATTACHMENTS, MAX_BODY_LENGTH, MAX_SUBJECT_LENGTH, addFeedback, dismissFeedback, editFeedback, listFeedback, listMyFeedback };
165
+ //# sourceMappingURL=service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../../src/modules/feedback/service.ts"],"sourcesContent":["import { getDbClient } from '../../db/client'\nimport type { Actor } from '../visitor/visitorIdentity'\n\n/** How long an author may edit their own wording. */\nexport const EDIT_WINDOW_MS = 60 * 60 * 1000\n\nexport const MAX_BODY_LENGTH = 1000\nexport const MAX_SUBJECT_LENGTH = 120\nexport const MAX_ATTACHMENTS = 3\n\n/** One piece of feedback as a client sees it. Never carries an author hash. */\nexport interface FeedbackView {\n id: string\n authorName: string\n /** Null when the visitor sent a bare flag with no words. */\n body: string | null\n /** What they were looking for, on a catalog-level ask. Null on entry feedback. */\n subject: string | null\n /** Slug of the entry this is about, or null for a catalog-level ask. */\n resourceSlug: string | null\n /** Display name of that entry, so the tracker can label a row without a second query. */\n resourceName: string | null\n attachmentIds: string[]\n status: 'applied' | 'acknowledged' | null\n reviewerReply: string | null\n /** When a maintainer last touched it — what \"unread\" is computed against. */\n reviewedAt: string | null\n createdAt: string\n editedAt: string | null\n /** Whether this browser wrote it — what makes Edit and Dismiss appear. */\n isMine: boolean\n /**\n * The absolute instant this browser's edit window closes, or null when it is not\n * theirs. Absolute rather than a remaining duration so a client with a skewed clock\n * hides the control at the same moment the server starts refusing the edit.\n */\n canEditUntil: string | null\n}\n\ntype Row = {\n id: string\n authorHash: string\n authorAlias: string\n body: string | null\n subject: string | null\n status: 'applied' | 'acknowledged' | null\n reviewerReply: string | null\n reviewedAt: Date | null\n editedAt: Date | null\n createdAt: Date\n resource?: { slug: string; displayName: string } | null\n attachments?: { id: string }[]\n}\n\nfunction toView(row: Row, actor: Actor | null): FeedbackView {\n const isMine = !!actor && row.authorHash === actor.hash\n return {\n id: row.id,\n authorName: row.authorAlias,\n body: row.body,\n subject: row.subject,\n resourceSlug: row.resource?.slug ?? null,\n resourceName: row.resource?.displayName ?? null,\n attachmentIds: (row.attachments ?? []).map((a) => a.id),\n status: row.status,\n reviewerReply: row.reviewerReply,\n reviewedAt: row.reviewedAt?.toISOString() ?? null,\n createdAt: row.createdAt.toISOString(),\n editedAt: row.editedAt?.toISOString() ?? null,\n isMine,\n canEditUntil: isMine\n ? new Date(row.createdAt.getTime() + EDIT_WINDOW_MS).toISOString()\n : null,\n }\n}\n\nconst WITH_RELATIONS = {\n resource: { select: { slug: true, displayName: true } },\n attachments: { select: { id: true } },\n} as const\n\nasync function resourceIdForSlug(slug: string): Promise<string | null> {\n const resource = await getDbClient().dbResource.findUnique({\n where: { slug },\n select: { id: true },\n })\n return resource?.id ?? null\n}\n\nexport interface FeedbackList {\n items: FeedbackView[]\n /** Unreviewed count — what the badge shows. `status IS NULL` is the whole definition. */\n openCount: number\n}\n\n/** Oldest first: a thread reads top to bottom. */\nexport async function listFeedback(\n resourceSlug: string,\n actor: Actor | null,\n): Promise<FeedbackList> {\n const resourceId = await resourceIdForSlug(resourceSlug)\n if (!resourceId) return { items: [], openCount: 0 }\n\n const rows = await getDbClient().dbFeedback.findMany({\n where: { resourceId },\n orderBy: { createdAt: 'asc' },\n include: WITH_RELATIONS,\n })\n return {\n items: rows.map((row) => toView(row, actor)),\n openCount: rows.filter((row) => row.status === null).length,\n }\n}\n\n/**\n * Everything this browser filed, across entries, for the tracker.\n *\n * Sorted the way the tracker renders: unreviewed first, then newest. Postgres sorts\n * NULLs last by default, so `status` ascending would bury the pending rows — hence the\n * explicit two-key ordering rather than relying on the default.\n */\nexport async function listMyFeedback(\n actor: Actor | null,\n): Promise<FeedbackView[]> {\n if (!actor) return []\n const rows = await getDbClient().dbFeedback.findMany({\n where: { authorHash: actor.hash },\n orderBy: [\n { status: { sort: 'asc', nulls: 'first' } },\n { createdAt: 'desc' },\n ],\n include: WITH_RELATIONS,\n })\n return rows.map((row) => toView(row, actor))\n}\n\nexport class FeedbackError extends Error {\n constructor(\n readonly reason:\n | 'not-found'\n | 'not-author'\n | 'window-closed'\n | 'already-reviewed'\n | 'empty'\n | 'too-many-attachments',\n message: string,\n ) {\n super(message)\n }\n}\n\nfunction normaliseBody(body: string | undefined | null): string | null {\n if (body === undefined || body === null) return null\n const trimmed = body.trim()\n // A bare flag is a valid signal, so empty is stored as absent rather than refused.\n return trimmed ? trimmed.slice(0, MAX_BODY_LENGTH) : null\n}\n\nexport interface AddFeedbackInput {\n /** Omitted for a catalog-level ask. */\n resourceSlug?: string\n /** Required when there is no resourceSlug — it is the substance of the ask. */\n subject?: string\n body?: string\n attachmentIds?: string[]\n}\n\nexport async function addFeedback(\n input: AddFeedbackInput,\n actor: Actor,\n): Promise<FeedbackView> {\n const { resourceSlug, subject, body, attachmentIds = [] } = input\n\n if (!resourceSlug && !subject?.trim()) {\n throw new FeedbackError(\n 'empty',\n 'Tell us which entry this is about, or what you were looking for',\n )\n }\n if (attachmentIds.length > MAX_ATTACHMENTS) {\n throw new FeedbackError(\n 'too-many-attachments',\n `Up to ${MAX_ATTACHMENTS} images`,\n )\n }\n\n let resourceId: string | null = null\n if (resourceSlug) {\n resourceId = await resourceIdForSlug(resourceSlug)\n if (!resourceId) {\n throw new FeedbackError('not-found', `Unknown resource: ${resourceSlug}`)\n }\n }\n\n const row = await getDbClient().$transaction(async (tx) => {\n const created = await tx.dbFeedback.create({\n data: {\n resourceId,\n subject: resourceId\n ? null\n : subject!.trim().slice(0, MAX_SUBJECT_LENGTH),\n authorHash: actor.hash,\n authorAlias: actor.alias,\n body: normaliseBody(body),\n },\n select: { id: true },\n })\n\n // Claim only images this browser uploaded and nobody has claimed yet, so an id\n // guessed from someone else's upload cannot be pulled onto your feedback.\n //\n // `updateMany` with the conditions in the WHERE, rather than a nested `connect`:\n // `connect` takes only the id, and because `feedbackId` is a single FK it would\n // MOVE a claimed image off its owner's feedback instead of refusing.\n if (attachmentIds.length > 0) {\n await tx.dbFeedbackAttachment.updateMany({\n where: {\n id: { in: attachmentIds },\n authorHash: actor.hash,\n feedbackId: null,\n },\n data: { feedbackId: created.id },\n })\n }\n\n return tx.dbFeedback.findUniqueOrThrow({\n where: { id: created.id },\n include: WITH_RELATIONS,\n })\n })\n return toView(row, actor)\n}\n\n/**\n * Load a row and check this actor may still change its wording.\n *\n * Enforced here, not in the UI: the control disappearing is a courtesy, and a request\n * that arrives a second late must still be refused.\n */\nasync function loadOwnForEdit(id: string, actor: Actor) {\n const row = await getDbClient().dbFeedback.findUnique({ where: { id } })\n if (!row) throw new FeedbackError('not-found', 'This no longer exists')\n if (row.authorHash !== actor.hash) {\n throw new FeedbackError('not-author', 'Only the author can change this')\n }\n if (Date.now() > row.createdAt.getTime() + EDIT_WINDOW_MS) {\n throw new FeedbackError(\n 'window-closed',\n 'Wording can only be changed in the first hour',\n )\n }\n return row\n}\n\nexport async function editFeedback(\n id: string,\n body: string,\n actor: Actor,\n): Promise<FeedbackView> {\n await loadOwnForEdit(id, actor)\n const row = await getDbClient().dbFeedback.update({\n where: { id },\n data: { body: normaliseBody(body), editedAt: new Date() },\n include: WITH_RELATIONS,\n })\n return toView(row, actor)\n}\n\n/**\n * Withdraw your own feedback.\n *\n * Allowed for as long as nobody has reviewed it, with no time limit — the edit window\n * exists to stop someone rewriting words others have already read, and that reasoning\n * does not apply to withdrawing something no maintainer has acted on. Once reviewed it\n * is public record carrying a maintainer's reply, so withdrawing would delete their\n * answer; from then on it is refused.\n */\nexport async function dismissFeedback(\n id: string,\n actor: Actor,\n): Promise<{ id: string }> {\n const row = await getDbClient().dbFeedback.findUnique({ where: { id } })\n if (!row) throw new FeedbackError('not-found', 'This no longer exists')\n if (row.authorHash !== actor.hash) {\n throw new FeedbackError('not-author', 'Only the author can withdraw this')\n }\n if (row.status !== null) {\n throw new FeedbackError(\n 'already-reviewed',\n 'This has been answered, so it stays on the entry',\n )\n }\n await getDbClient().dbFeedback.delete({ where: { id } })\n return { id }\n}\n"],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ //#region src/modules/visitor/visitorIdentity.d.ts
2
+ /** Opaque per-browser token. httpOnly, so page scripts cannot read or forge it. */
3
+ declare const VISITOR_COOKIE = "ac_cid";
4
+ /**
5
+ * Who the server thinks is commenting.
6
+ *
7
+ * The catalog is browsable without logging in, so there is no session to hang a
8
+ * comment on. `hash` is the only thing that authorises editing or deleting a
9
+ * comment later, and `alias` is the only name a reader ever sees.
10
+ */
11
+ interface Visitor {
12
+ hash: string;
13
+ alias: string;
14
+ }
15
+ /** Who the server thinks is acting, whether or not they ever signed in. */
16
+ type ActorKind = 'user' | 'visitor';
17
+ /**
18
+ * One identity for every surface that needs one.
19
+ *
20
+ * `hash` is the secret-derived half: it authorises editing and withdrawing, is stored on
21
+ * rows, and never leaves the server. `publicId` is the half a client may hold — it
22
+ * identifies without authorising, which is what analytics needs and what the httpOnly
23
+ * cookie deliberately cannot give it. Same split better-auth already uses between its
24
+ * session cookie and `user.id`.
25
+ */
26
+ interface Actor {
27
+ hash: string;
28
+ publicId: string;
29
+ alias: string;
30
+ kind: ActorKind;
31
+ }
32
+ /**
33
+ * Read the visitor token, issuing one when this browser has none yet.
34
+ *
35
+ * Called on every tRPC request rather than only when commenting: the cookie has to
36
+ * already exist by the time someone clicks Post, and issuing it on the read that
37
+ * renders the comment list is the one moment we know a page is open.
38
+ *
39
+ * `res` is absent on internally-created contexts (see createAcMiddleware), and
40
+ * those never comment — they get an identity derived from the token they carry, if
41
+ * any, and cannot be issued a new one.
42
+ */
43
+ declare function resolveVisitor(req: {
44
+ headers: {
45
+ cookie?: string;
46
+ };
47
+ }, res?: {
48
+ cookie: (name: string, value: string, options: Record<string, unknown>) => unknown;
49
+ }): Visitor | null;
50
+ /**
51
+ * A non-authorising id derived from the hash.
52
+ *
53
+ * Safe to hand a client: knowing it proves nothing and authorises nothing, because the
54
+ * check is always against the full hash of the cookie the browser actually sent.
55
+ */
56
+ declare function publicIdFromHash(hash: string): string;
57
+ /**
58
+ * Who is acting, signed in or not.
59
+ *
60
+ * `userId` is passed in rather than looked up, so this stays synchronous and testable in
61
+ * a node environment — the caller already has the session when it builds the request
62
+ * context. When login arrives this starts returning `kind: 'user'` and nothing
63
+ * downstream has to change.
64
+ */
65
+ declare function resolveActor(req: {
66
+ headers: {
67
+ cookie?: string;
68
+ };
69
+ }, res?: Parameters<typeof resolveVisitor>[1], userId?: string | null): Actor | null;
70
+ /**
71
+ * A deterministic identity from a seed, for tests and local development.
72
+ *
73
+ * Returns the raw token *and* its hash, so a database fixture and a browser cookie can
74
+ * refer to the same person: seed rows with `hash`, hand the browser `token`. Without
75
+ * this, nothing scoped to one visitor is testable — the cookie is httpOnly on purpose,
76
+ * so page scripts cannot set it.
77
+ */
78
+ declare function visitorFromSeed(seed: string): {
79
+ token: string;
80
+ hash: string;
81
+ publicId: string;
82
+ alias: string;
83
+ };
84
+ //#endregion
85
+ export { Actor, ActorKind, VISITOR_COOKIE, Visitor, publicIdFromHash, resolveActor, resolveVisitor, visitorFromSeed };
86
+ //# sourceMappingURL=visitorIdentity.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitorIdentity.d.mts","names":[],"sources":["../../../src/modules/visitor/visitorIdentity.ts"],"mappings":";;cAIa,cAAA;;;;;AAWb;;;UAAiB,OAAA;EACf,IAAA;EACA,KAAA;AAAA;;KAIU,SAAA;;AAWZ;;;;;;;;UAAiB,KAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA;EACA,IAAA,EAAM,SAAA;AAAA;;;;;;AAwIR;;;;;AAYA;iBA7CgB,cAAA,CACd,GAAA;EAAO,OAAA;IAAW,MAAA;EAAA;AAAA,GAClB,GAAA;EACE,MAAA,GACE,IAAA,UACA,KAAA,UACA,OAAA,EAAS,MAAA;AAAA,IAGZ,OAAA;;;;;;;iBAwBa,gBAAA,CAAiB,IAAA;;;;AA2CjC;;;;;iBA/BgB,YAAA,CACd,GAAA;EAAO,OAAA;IAAW,MAAA;EAAA;AAAA,GAClB,GAAA,GAAM,UAAA,QAAkB,cAAA,MACxB,MAAA,mBACC,KAAA;;;;;;;;;iBA2Ba,eAAA,CAAgB,IAAA;EAC9B,KAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA;AAAA"}
@@ -1,7 +1,7 @@
1
1
  import { getCookie } from "../../utils/cookies.mjs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
 
4
- //#region src/modules/comments/visitorIdentity.ts
4
+ //#region src/modules/visitor/visitorIdentity.ts
5
5
  /** Opaque per-browser token. httpOnly, so page scripts cannot read or forge it. */
6
6
  const VISITOR_COOKIE = "ac_cid";
7
7
  const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
@@ -115,7 +115,58 @@ function resolveVisitor(req, res) {
115
115
  alias: aliasForHash(hash)
116
116
  };
117
117
  }
118
+ /**
119
+ * A non-authorising id derived from the hash.
120
+ *
121
+ * Safe to hand a client: knowing it proves nothing and authorises nothing, because the
122
+ * check is always against the full hash of the cookie the browser actually sent.
123
+ */
124
+ function publicIdFromHash(hash) {
125
+ return hash.slice(0, 16);
126
+ }
127
+ /**
128
+ * Who is acting, signed in or not.
129
+ *
130
+ * `userId` is passed in rather than looked up, so this stays synchronous and testable in
131
+ * a node environment — the caller already has the session when it builds the request
132
+ * context. When login arrives this starts returning `kind: 'user'` and nothing
133
+ * downstream has to change.
134
+ */
135
+ function resolveActor(req, res, userId) {
136
+ if (userId) return {
137
+ hash: hashToken(`user:${userId}`),
138
+ publicId: publicIdFromHash(hashToken(`user:${userId}`)),
139
+ alias: aliasForHash(hashToken(`user:${userId}`)),
140
+ kind: "user"
141
+ };
142
+ const visitor = resolveVisitor(req, res);
143
+ if (!visitor) return null;
144
+ return {
145
+ hash: visitor.hash,
146
+ publicId: publicIdFromHash(visitor.hash),
147
+ alias: visitor.alias,
148
+ kind: "visitor"
149
+ };
150
+ }
151
+ /**
152
+ * A deterministic identity from a seed, for tests and local development.
153
+ *
154
+ * Returns the raw token *and* its hash, so a database fixture and a browser cookie can
155
+ * refer to the same person: seed rows with `hash`, hand the browser `token`. Without
156
+ * this, nothing scoped to one visitor is testable — the cookie is httpOnly on purpose,
157
+ * so page scripts cannot set it.
158
+ */
159
+ function visitorFromSeed(seed) {
160
+ const token = createHash("sha256").update(`seed:${seed}`).digest("hex");
161
+ const hash = hashToken(token);
162
+ return {
163
+ token,
164
+ hash,
165
+ publicId: publicIdFromHash(hash),
166
+ alias: aliasForHash(hash)
167
+ };
168
+ }
118
169
 
119
170
  //#endregion
120
- export { resolveVisitor };
171
+ export { VISITOR_COOKIE, publicIdFromHash, resolveActor, resolveVisitor, visitorFromSeed };
121
172
  //# sourceMappingURL=visitorIdentity.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitorIdentity.mjs","names":[],"sources":["../../../src/modules/visitor/visitorIdentity.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto'\nimport { getCookie } from '../../utils/cookies'\n\n/** Opaque per-browser token. httpOnly, so page scripts cannot read or forge it. */\nexport const VISITOR_COOKIE = 'ac_cid'\n\nconst ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000\n\n/**\n * Who the server thinks is commenting.\n *\n * The catalog is browsable without logging in, so there is no session to hang a\n * comment on. `hash` is the only thing that authorises editing or deleting a\n * comment later, and `alias` is the only name a reader ever sees.\n */\nexport interface Visitor {\n hash: string\n alias: string\n}\n\n/** Who the server thinks is acting, whether or not they ever signed in. */\nexport type ActorKind = 'user' | 'visitor'\n\n/**\n * One identity for every surface that needs one.\n *\n * `hash` is the secret-derived half: it authorises editing and withdrawing, is stored on\n * rows, and never leaves the server. `publicId` is the half a client may hold — it\n * identifies without authorising, which is what analytics needs and what the httpOnly\n * cookie deliberately cannot give it. Same split better-auth already uses between its\n * session cookie and `user.id`.\n */\nexport interface Actor {\n hash: string\n publicId: string\n alias: string\n kind: ActorKind\n}\n\nconst ADJECTIVES = [\n 'Ancient',\n 'Brave',\n 'Calm',\n 'Clever',\n 'Curious',\n 'Eager',\n 'Gentle',\n 'Happy',\n 'Honest',\n 'Humble',\n 'Jolly',\n 'Keen',\n 'Kind',\n 'Lively',\n 'Loyal',\n 'Merry',\n 'Mighty',\n 'Modest',\n 'Noble',\n 'Patient',\n 'Polite',\n 'Proud',\n 'Quiet',\n 'Rapid',\n 'Silent',\n 'Smooth',\n 'Steady',\n 'Subtle',\n 'Sunny',\n 'Swift',\n 'Tidy',\n 'Witty',\n]\n\nconst ANIMALS = [\n 'Badger',\n 'Beaver',\n 'Bison',\n 'Crane',\n 'Dolphin',\n 'Falcon',\n 'Ferret',\n 'Finch',\n 'Fox',\n 'Gecko',\n 'Heron',\n 'Ibex',\n 'Jackal',\n 'Kestrel',\n 'Lemur',\n 'Lynx',\n 'Magpie',\n 'Marten',\n 'Mole',\n 'Narwhal',\n 'Ocelot',\n 'Osprey',\n 'Otter',\n 'Panda',\n 'Puffin',\n 'Raven',\n 'Seal',\n 'Shrew',\n 'Stoat',\n 'Tapir',\n 'Vole',\n 'Walrus',\n]\n\n/**\n * A stable, readable name for a visitor hash, e.g. \"Curious Ferret\".\n *\n * Deterministic, so the same browser keeps the same name across comments — but it\n * is derived once and stored on the row, because widening these lists later would\n * otherwise rename every comment ever written.\n */\nexport function aliasForHash(hash: string): string {\n // Two independent slices of the hash, so the two words vary independently.\n const adjective =\n ADJECTIVES[parseInt(hash.slice(0, 8), 16) % ADJECTIVES.length]\n const animal = ANIMALS[parseInt(hash.slice(8, 16), 16) % ANIMALS.length]\n return `${adjective} ${animal}`\n}\n\nfunction hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\n/**\n * Read the visitor token, issuing one when this browser has none yet.\n *\n * Called on every tRPC request rather than only when commenting: the cookie has to\n * already exist by the time someone clicks Post, and issuing it on the read that\n * renders the comment list is the one moment we know a page is open.\n *\n * `res` is absent on internally-created contexts (see createAcMiddleware), and\n * those never comment — they get an identity derived from the token they carry, if\n * any, and cannot be issued a new one.\n */\nexport function resolveVisitor(\n req: { headers: { cookie?: string } },\n res?: {\n cookie: (\n name: string,\n value: string,\n options: Record<string, unknown>,\n ) => unknown\n },\n): Visitor | null {\n let token = getCookie(req, VISITOR_COOKIE)\n\n if (!token) {\n if (!res) return null\n token = randomUUID()\n res.cookie(VISITOR_COOKIE, token, {\n httpOnly: true,\n sameSite: 'lax',\n path: '/',\n maxAge: ONE_YEAR_MS,\n })\n }\n\n const hash = hashToken(token)\n return { hash, alias: aliasForHash(hash) }\n}\n\n/**\n * A non-authorising id derived from the hash.\n *\n * Safe to hand a client: knowing it proves nothing and authorises nothing, because the\n * check is always against the full hash of the cookie the browser actually sent.\n */\nexport function publicIdFromHash(hash: string): string {\n return hash.slice(0, 16)\n}\n\n/**\n * Who is acting, signed in or not.\n *\n * `userId` is passed in rather than looked up, so this stays synchronous and testable in\n * a node environment — the caller already has the session when it builds the request\n * context. When login arrives this starts returning `kind: 'user'` and nothing\n * downstream has to change.\n */\nexport function resolveActor(\n req: { headers: { cookie?: string } },\n res?: Parameters<typeof resolveVisitor>[1],\n userId?: string | null,\n): Actor | null {\n if (userId) {\n return {\n hash: hashToken(`user:${userId}`),\n publicId: publicIdFromHash(hashToken(`user:${userId}`)),\n alias: aliasForHash(hashToken(`user:${userId}`)),\n kind: 'user',\n }\n }\n const visitor = resolveVisitor(req, res)\n if (!visitor) return null\n return {\n hash: visitor.hash,\n publicId: publicIdFromHash(visitor.hash),\n alias: visitor.alias,\n kind: 'visitor',\n }\n}\n\n/**\n * A deterministic identity from a seed, for tests and local development.\n *\n * Returns the raw token *and* its hash, so a database fixture and a browser cookie can\n * refer to the same person: seed rows with `hash`, hand the browser `token`. Without\n * this, nothing scoped to one visitor is testable — the cookie is httpOnly on purpose,\n * so page scripts cannot set it.\n */\nexport function visitorFromSeed(seed: string): {\n token: string\n hash: string\n publicId: string\n alias: string\n} {\n const token = createHash('sha256').update(`seed:${seed}`).digest('hex')\n const hash = hashToken(token)\n return {\n token,\n hash,\n publicId: publicIdFromHash(hash),\n alias: aliasForHash(hash),\n }\n}\n"],"mappings":";;;;;AAIA,MAAa,iBAAiB;AAE9B,MAAM,cAAc,MAAM,KAAK,KAAK,KAAK;AAiCzC,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,UAAU;CACd;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,SAAgB,aAAa,MAAsB;AAKjD,QAAO,GAFL,WAAW,SAAS,KAAK,MAAM,GAAG,EAAE,EAAE,GAAG,GAAG,WAAW,QAErC,GADL,QAAQ,SAAS,KAAK,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,QAAQ;;AAInE,SAAS,UAAU,OAAuB;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;;;;;;;;;;;;AAczD,SAAgB,eACd,KACA,KAOgB;CAChB,IAAI,QAAQ,UAAU,KAAK,eAAe;AAE1C,KAAI,CAAC,OAAO;AACV,MAAI,CAAC,IAAK,QAAO;AACjB,UAAQ,YAAY;AACpB,MAAI,OAAO,gBAAgB,OAAO;GAChC,UAAU;GACV,UAAU;GACV,MAAM;GACN,QAAQ;GACT,CAAC;;CAGJ,MAAM,OAAO,UAAU,MAAM;AAC7B,QAAO;EAAE;EAAM,OAAO,aAAa,KAAK;EAAE;;;;;;;;AAS5C,SAAgB,iBAAiB,MAAsB;AACrD,QAAO,KAAK,MAAM,GAAG,GAAG;;;;;;;;;;AAW1B,SAAgB,aACd,KACA,KACA,QACc;AACd,KAAI,OACF,QAAO;EACL,MAAM,UAAU,QAAQ,SAAS;EACjC,UAAU,iBAAiB,UAAU,QAAQ,SAAS,CAAC;EACvD,OAAO,aAAa,UAAU,QAAQ,SAAS,CAAC;EAChD,MAAM;EACP;CAEH,MAAM,UAAU,eAAe,KAAK,IAAI;AACxC,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO;EACL,MAAM,QAAQ;EACd,UAAU,iBAAiB,QAAQ,KAAK;EACxC,OAAO,QAAQ;EACf,MAAM;EACP;;;;;;;;;;AAWH,SAAgB,gBAAgB,MAK9B;CACA,MAAM,QAAQ,WAAW,SAAS,CAAC,OAAO,QAAQ,OAAO,CAAC,OAAO,MAAM;CACvE,MAAM,OAAO,UAAU,MAAM;AAC7B,QAAO;EACL;EACA;EACA,UAAU,iBAAiB,KAAK;EAChC,OAAO,aAAa,KAAK;EAC1B"}
@@ -1,5 +1,5 @@
1
1
  import { AppCatalogCompanySpecificBackend } from "../types/backend/companySpecificBackend.mjs";
2
- import { Visitor } from "../modules/comments/visitorIdentity.mjs";
2
+ import { Actor } from "../modules/visitor/visitorIdentity.mjs";
3
3
  import { User } from "better-auth/types";
4
4
 
5
5
  //#region src/server/acTrpcContext.d.ts
@@ -8,23 +8,24 @@ interface AcTrpcContext {
8
8
  user: User | null;
9
9
  isAdmin: boolean;
10
10
  /**
11
- * The anonymous browser behind this request, independent of `user` — the catalog
12
- * is browsable logged out, and comments are attributed to this instead. Null when
13
- * the context was built without a response to issue the cookie on.
11
+ * Who is acting on this request — the signed-in user when there is one, otherwise the
12
+ * anonymous browser behind it. The catalog is browsable logged out and feedback is
13
+ * attributed to this rather than to `user`. Null when the context was built without a
14
+ * response to issue the cookie on.
14
15
  */
15
- visitor: Visitor | null;
16
+ actor: Actor | null;
16
17
  }
17
18
  interface AcTrpcContextOptions {
18
19
  companySpecificBackend: AppCatalogCompanySpecificBackend;
19
20
  user?: User | null;
20
21
  isAdmin?: boolean;
21
- visitor?: Visitor | null;
22
+ actor?: Actor | null;
22
23
  }
23
24
  declare function createAcTrpcContext({
24
25
  companySpecificBackend,
25
26
  user,
26
27
  isAdmin,
27
- visitor
28
+ actor
28
29
  }: AcTrpcContextOptions): AcTrpcContext;
29
30
  //#endregion
30
31
  export { AcTrpcContext, AcTrpcContextOptions, createAcTrpcContext };
@@ -1 +1 @@
1
- {"version":3,"file":"acTrpcContext.d.mts","names":[],"sources":["../../src/server/acTrpcContext.ts"],"mappings":";;;;;UAIiB,aAAA;EACf,sBAAA,EAAwB,gCAAA;EACxB,IAAA,EAAM,IAAA;EACN,OAAA;;;;;;EAMA,OAAA,EAAS,OAAA;AAAA;AAAA,UAGM,oBAAA;EACf,sBAAA,EAAwB,gCAAA;EACxB,IAAA,GAAO,IAAA;EACP,OAAA;EACA,OAAA,GAAU,OAAA;AAAA;AAAA,iBAGI,mBAAA,CAAA;EACd,sBAAA;EACA,IAAA;EACA,OAAA;EACA;AAAA,GACC,oBAAA,GAAuB,aAAA"}
1
+ {"version":3,"file":"acTrpcContext.d.mts","names":[],"sources":["../../src/server/acTrpcContext.ts"],"mappings":";;;;;UAIiB,aAAA;EACf,sBAAA,EAAwB,gCAAA;EACxB,IAAA,EAAM,IAAA;EACN,OAAA;;;;;;;EAOA,KAAA,EAAO,KAAA;AAAA;AAAA,UAGQ,oBAAA;EACf,sBAAA,EAAwB,gCAAA;EACxB,IAAA,GAAO,IAAA;EACP,OAAA;EACA,KAAA,GAAQ,KAAA;AAAA;AAAA,iBAGM,mBAAA,CAAA;EACd,sBAAA;EACA,IAAA;EACA,OAAA;EACA;AAAA,GACC,oBAAA,GAAuB,aAAA"}
@@ -1,10 +1,10 @@
1
1
  //#region src/server/acTrpcContext.ts
2
- function createAcTrpcContext({ companySpecificBackend, user = null, isAdmin = false, visitor = null }) {
2
+ function createAcTrpcContext({ companySpecificBackend, user = null, isAdmin = false, actor = null }) {
3
3
  return {
4
4
  companySpecificBackend,
5
5
  user,
6
6
  isAdmin,
7
- visitor
7
+ actor
8
8
  };
9
9
  }
10
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"acTrpcContext.mjs","names":[],"sources":["../../src/server/acTrpcContext.ts"],"sourcesContent":["import type { AppCatalogCompanySpecificBackend } from '../types'\nimport type { Visitor } from '../modules/comments/visitorIdentity'\nimport type { User } from 'better-auth/types'\n\nexport interface AcTrpcContext {\n companySpecificBackend: AppCatalogCompanySpecificBackend\n user: User | null\n isAdmin: boolean\n /**\n * The anonymous browser behind this request, independent of `user` — the catalog\n * is browsable logged out, and comments are attributed to this instead. Null when\n * the context was built without a response to issue the cookie on.\n */\n visitor: Visitor | null\n}\n\nexport interface AcTrpcContextOptions {\n companySpecificBackend: AppCatalogCompanySpecificBackend\n user?: User | null\n isAdmin?: boolean\n visitor?: Visitor | null\n}\n\nexport function createAcTrpcContext({\n companySpecificBackend,\n user = null,\n isAdmin = false,\n visitor = null,\n}: AcTrpcContextOptions): AcTrpcContext {\n return { companySpecificBackend, user, isAdmin, visitor }\n}\n"],"mappings":";AAuBA,SAAgB,oBAAoB,EAClC,wBACA,OAAO,MACP,UAAU,OACV,UAAU,QAC4B;AACtC,QAAO;EAAE;EAAwB;EAAM;EAAS;EAAS"}
1
+ {"version":3,"file":"acTrpcContext.mjs","names":[],"sources":["../../src/server/acTrpcContext.ts"],"sourcesContent":["import type { AppCatalogCompanySpecificBackend } from '../types'\nimport type { Actor } from '../modules/visitor/visitorIdentity'\nimport type { User } from 'better-auth/types'\n\nexport interface AcTrpcContext {\n companySpecificBackend: AppCatalogCompanySpecificBackend\n user: User | null\n isAdmin: boolean\n /**\n * Who is acting on this request — the signed-in user when there is one, otherwise the\n * anonymous browser behind it. The catalog is browsable logged out and feedback is\n * attributed to this rather than to `user`. Null when the context was built without a\n * response to issue the cookie on.\n */\n actor: Actor | null\n}\n\nexport interface AcTrpcContextOptions {\n companySpecificBackend: AppCatalogCompanySpecificBackend\n user?: User | null\n isAdmin?: boolean\n actor?: Actor | null\n}\n\nexport function createAcTrpcContext({\n companySpecificBackend,\n user = null,\n isAdmin = false,\n actor = null,\n}: AcTrpcContextOptions): AcTrpcContext {\n return { companySpecificBackend, user, isAdmin, actor }\n}\n"],"mappings":";AAwBA,SAAgB,oBAAoB,EAClC,wBACA,OAAO,MACP,UAAU,OACV,QAAQ,QAC8B;AACtC,QAAO;EAAE;EAAwB;EAAM;EAAS;EAAO"}
@@ -1,7 +1,7 @@
1
1
  import { AppCatalogData, Resource } from "../types/common/appCatalogTypes.mjs";
2
2
  import { AcTrpcContext } from "./acTrpcContext.mjs";
3
3
  import { BetterAuth } from "../modules/auth/auth.mjs";
4
- import { CommentView } from "../modules/comments/service.mjs";
4
+ import { FeedbackList, FeedbackView } from "../modules/feedback/service.mjs";
5
5
  import * as _trpc_server0 from "@trpc/server";
6
6
  import * as better_auth0 from "better-auth";
7
7
 
@@ -54,7 +54,7 @@ declare function createTrpcRouter(auth?: BetterAuth, options?: {
54
54
  meta: object;
55
55
  }>;
56
56
  }>>;
57
- comments: _trpc_server0.TRPCBuiltRouter<{
57
+ feedback: _trpc_server0.TRPCBuiltRouter<{
58
58
  ctx: AcTrpcContext;
59
59
  meta: object;
60
60
  errorShape: _trpc_server0.TRPCDefaultErrorShape;
@@ -64,15 +64,22 @@ declare function createTrpcRouter(auth?: BetterAuth, options?: {
64
64
  input: {
65
65
  resourceSlug: string;
66
66
  };
67
- output: CommentView[];
67
+ output: FeedbackList;
68
+ meta: object;
69
+ }>;
70
+ mine: _trpc_server0.TRPCQueryProcedure<{
71
+ input: void;
72
+ output: FeedbackView[];
68
73
  meta: object;
69
74
  }>;
70
75
  add: _trpc_server0.TRPCMutationProcedure<{
71
76
  input: {
72
- resourceSlug: string;
73
- body: string;
77
+ resourceSlug?: string | undefined;
78
+ subject?: string | undefined;
79
+ body?: string | undefined;
80
+ attachmentIds?: string[] | undefined;
74
81
  };
75
- output: CommentView;
82
+ output: FeedbackView;
76
83
  meta: object;
77
84
  }>;
78
85
  edit: _trpc_server0.TRPCMutationProcedure<{
@@ -80,10 +87,10 @@ declare function createTrpcRouter(auth?: BetterAuth, options?: {
80
87
  id: string;
81
88
  body: string;
82
89
  };
83
- output: CommentView;
90
+ output: FeedbackView;
84
91
  meta: object;
85
92
  }>;
86
- remove: _trpc_server0.TRPCMutationProcedure<{
93
+ dismiss: _trpc_server0.TRPCMutationProcedure<{
87
94
  input: {
88
95
  id: string;
89
96
  };
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.mts","names":[],"sources":["../../src/server/controller.ts"],"mappings":";;;;;;;;;;;;iBAmCgB,gBAAA,CACd,IAAA,GAAO,UAAA,EACP,OAAA;EAAY,eAAA;AAAA,kBAA2B,eAAA;OADtB,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwCP,UAAA,GAAa,UAAA,QAAkB,gBAAA"}
1
+ {"version":3,"file":"controller.d.mts","names":[],"sources":["../../src/server/controller.ts"],"mappings":";;;;;;;;;;;;iBAmCgB,gBAAA,CACd,IAAA,GAAO,UAAA,EACP,OAAA;EAAY,eAAA;AAAA,kBAA2B,eAAA;OADtB,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwCP,UAAA,GAAa,UAAA,QAAkB,gBAAA"}
@@ -1,7 +1,7 @@
1
1
  import { getAppCatalogData, updateAiMemory, updateApp } from "../modules/appCatalog/service.mjs";
2
2
  import { createAuthRouter } from "../modules/auth/authRouter.mjs";
3
3
  import { publicProcedure, router, t } from "./trpcSetup.mjs";
4
- import { createCommentsRouter } from "../modules/comments/commentsRouter.mjs";
4
+ import { createFeedbackRouter } from "../modules/feedback/feedbackRouter.mjs";
5
5
  import { z } from "zod";
6
6
 
7
7
  //#region src/server/controller.ts
@@ -42,7 +42,7 @@ function createTrpcRouter(auth, options) {
42
42
  return updateAiMemory(input);
43
43
  })
44
44
  }),
45
- comments: createCommentsRouter(),
45
+ feedback: createFeedbackRouter(),
46
46
  auth: createAuthRouter(t, auth, options)
47
47
  });
48
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"controller.mjs","names":["updateAppService","updateAiMemoryService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n updateAiMemory as updateAiMemoryService,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { Resource } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createCommentsRouter } from '../modules/comments/commentsRouter'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: { devLoginEnabled?: boolean },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAppService(input)\n }),\n updateAiMemory: publicProcedure\n .input(\n z.object({\n id: z.string(),\n aiMemory: z.string().nullable(),\n }),\n )\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAiMemoryService(input)\n }),\n }),\n\n comments: createCommentsRouter(),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;;AAcA,MAAM,uBAAuB,EAAE,OAAO;CACpC,IAAI,EAAE,QAAQ;CACd,MAAM,EACH,OAAO;EACN,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,cAAc,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU;EACtD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU;EAC7B,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;EACvC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;EAC3C,CAAC,CACD,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC,SAAS,GAAG,EACxC,SAAS,+BACV,CAAC;CACL,CAAC;;;;;AAMF,SAAgB,iBACd,MACA,SACA;AACA,QAAO,OAAO;EACZ,YAAY,OAAO;GACjB,SAAS,gBAAgB,MACvB,OAAO,EAAE,UAAmC;IAC1C,MAAM,WAAW,MAAM,mBAAmB;IAC1C,MAAM,WAAW,MAAM,IAAI,uBAAuB,kBAAkB;AAEpE,WAAO;KACL,GAAG;KACH,GAAI,YAAY,EAAE,UAAU;KAC7B;KAEJ;GACD,WAAW,gBACR,MAAM,qBAAqB,CAC3B,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOA,UAAiB,MAAM;KAC9B;GACJ,gBAAgB,gBACb,MACC,EAAE,OAAO;IACP,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,QAAQ,CAAC,UAAU;IAChC,CAAC,CACH,CACA,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOC,eAAsB,MAAM;KACnC;GACL,CAAC;EAEF,UAAU,sBAAsB;EAGhC,MAAM,iBAAiB,GAAG,MAAM,QAAQ;EACzC,CAAC"}
1
+ {"version":3,"file":"controller.mjs","names":["updateAppService","updateAiMemoryService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n updateAiMemory as updateAiMemoryService,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { Resource } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createFeedbackRouter } from '../modules/feedback/feedbackRouter'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: { devLoginEnabled?: boolean },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAppService(input)\n }),\n updateAiMemory: publicProcedure\n .input(\n z.object({\n id: z.string(),\n aiMemory: z.string().nullable(),\n }),\n )\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAiMemoryService(input)\n }),\n }),\n\n feedback: createFeedbackRouter(),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;;AAcA,MAAM,uBAAuB,EAAE,OAAO;CACpC,IAAI,EAAE,QAAQ;CACd,MAAM,EACH,OAAO;EACN,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,cAAc,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU;EACtD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU;EAC7B,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;EACvC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;EAC3C,CAAC,CACD,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC,SAAS,GAAG,EACxC,SAAS,+BACV,CAAC;CACL,CAAC;;;;;AAMF,SAAgB,iBACd,MACA,SACA;AACA,QAAO,OAAO;EACZ,YAAY,OAAO;GACjB,SAAS,gBAAgB,MACvB,OAAO,EAAE,UAAmC;IAC1C,MAAM,WAAW,MAAM,mBAAmB;IAC1C,MAAM,WAAW,MAAM,IAAI,uBAAuB,kBAAkB;AAEpE,WAAO;KACL,GAAG;KACH,GAAI,YAAY,EAAE,UAAU;KAC7B;KAEJ;GACD,WAAW,gBACR,MAAM,qBAAqB,CAC3B,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOA,UAAiB,MAAM;KAC9B;GACJ,gBAAgB,gBACb,MACC,EAAE,OAAO;IACP,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,QAAQ,CAAC,UAAU;IAChC,CAAC,CACH,CACA,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOC,eAAsB,MAAM;KACnC;GACL,CAAC;EAEF,UAAU,sBAAsB;EAGhC,MAAM,iBAAiB,GAAG,MAAM,QAAQ;EACzC,CAAC"}
@@ -170,6 +170,18 @@ interface Resource {
170
170
  tiers?: TierVariant[];
171
171
  /** Deep links that take one typed identifier — see {@link QuickJump}. */
172
172
  quickJumps?: QuickJump[];
173
+ /**
174
+ * What this resource's children are called, as a plural noun ("Accounts",
175
+ * "Namespaces", "Databases"). Set on the PARENT, not the children.
176
+ *
177
+ * The detail card names its children tab with this. Children are modelled as
178
+ * plain resources carrying a `parentSlug`, so nothing in the data says what
179
+ * kind of thing they are — and a tab reading "Accounts 430" tells the reader
180
+ * what is in there, where the generic fallback tells them nothing. Omit it and
181
+ * the card says "Resources"; only the handful of entries that actually have
182
+ * children need to set it.
183
+ */
184
+ childrenLabel?: string;
173
185
  /** Slug of parent resource (undefined for top-level applications) */
174
186
  parentSlug?: string;
175
187
  /** Tier slug (e.g. "prod", "dev") — for sub-resources */
@@ -1 +1 @@
1
- {"version":3,"file":"appCatalogTypes.d.mts","names":[],"sources":["../../../src/types/common/appCatalogTypes.ts"],"mappings":";;;;;;;;UAmBiB,WAAA;EACf,QAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA;EACA,aAAA,GAAgB,gBAAA;AAAA;;;;;;AAqClB;;;;;AAWA;;UA7BiB,SAAA;EAuCY;;;;;;EAhC3B,QAAA;EAgC2B;EA9B3B,KAAA;EACA,GAAA;AAAA;;UAQe,WAAA;EAyBQ;EAvBvB,IAAA;EACA,OAAA;AAAA;;;AAoCF;;;UA5BiB,cAAA;EAgCN;EA9BT,aAAA;EA0CmB;EAxCnB,cAAA;EAyBA;EAvBA,mBAAA;EAyBA;EAvBA,kBAAA;EAwBS;EAtBT,aAAA,GAAgB,WAAA;AAAA;;KAIN,aAAA;;KAQA,gBAAA;;;AAkCZ;;UA5BiB,WAAA;EACf,KAAA,EAAO,gBAAA;EA4BP;EA1BA,aAAA;EACA,OAAA,EAAS,aAAA;EACT,aAAA;EA4BW;EA1BX,KAAA;EACA,mBAAA;EA2BmB;;AAOrB;;;EA5BE,yBAAA;EA8BA;EA5BA,MAAA,EAAQ,WAAA;AAAA;;;AA4CV;;;;;;;UAhCiB,eAAA;EACf,UAAA;EACA,GAAA;EACA,SAAA;EA8BA;EA5BA,QAAA,GAAW,cAAA;EA+BX;EA7BA,KAAA,GAAQ,WAAA;AAAA;;;;;UAOO,SAAA;EA6Bf;EA3BA,aAAA;EAqCA;;;;;;EA9BA,mBAAA;EAmCA;EAjCA,OAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAwCQ;EAtCR,IAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA,GAAgB,gBAAA;EAChB,KAAA;EAiDA;;;;;;;;EAxCA,UAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;IAAU,GAAA;IAAa,KAAA;EAAA;EACvB,QAAA;EACA,aAAA;EACA,OAAA,cAAqB,eAAA;EACrB,UAAA;IAqEwB,0HAnEtB,IAAA,iCAiEF;IA/DE,eAAA,WAiEF;IA/DE,OAAA;EAAA;EA+DsB;EA5DxB,QAAA;EA+DmB;EA7DnB,QAAA;EA6DiD;EA3DjD,SAAA;EA4DU;EA1DV,KAAA,GAAQ,WAAA;EA0DF;EAxDN,UAAA,GAAa,SAAA;EAuDO;EAnDpB,UAAA;EAmDiD;EAjDjD,IAAA;EAkDO;EAhDP,OAAA;EAgDc;EA9Cd,eAAA;EAiDU;EA/CV,aAAA;;EAEA,cAAA;EA8Cc;EA5Cd,KAAA,GAAQ,MAAA;EAgDuB;;;;;;EAzC/B,SAAA,GAAY,SAAA;EA4CD;AAGb;;;;EAzCE,aAAA;EACA,cAAA;EACA,mBAAA;EA6CA;;;AAGF;;EA1CE,cAAA;EA2CU;EAzCV,SAAA;AAAA;AAAA,UAIe,WAAA;EACf,EAAA;EACA,IAAA;AAAA;AAAA,UAGe,qBAAA;EACf,MAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;AAAA,KAGL,gBAAA,8BAA8C,CAAA,eAC/C,IAAA,CAAK,CAAA,EAAG,IAAA;AAAA,KAGA,iBAAA,GAAoB,gBAAA,CAC9B,cAAA;AAAA,UAIe,gBAAA;EACf,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,WAAA;EACf,WAAA;EACA,GAAA;EAmByB;EAjBzB,GAAA;EAYW;EAVX,MAAA;AAAA;AAAA,UAGe,cAAA;EACf,OAAA,GAAU,WAAA;EACV,QAAA,GAAW,WAAA;EACX,WAAA,GAAc,WAAA;AAAA;AAAA,UAGC,cAAA;EACf,SAAA,EAAW,QAAA;EACX,eAAA,EAAiB,qBAAA;EACjB,eAAA,EAAiB,iBAAA;EACjB,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,KAAA;EACR,QAAA,GAAW,cAAA;AAAA"}
1
+ {"version":3,"file":"appCatalogTypes.d.mts","names":[],"sources":["../../../src/types/common/appCatalogTypes.ts"],"mappings":"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"}
@@ -38,6 +38,18 @@ type ApprovalMethod = {
38
38
  * Displayed approval method
39
39
  */
40
40
  displayName: string;
41
+ /**
42
+ * One or two words for places that cannot afford `displayName`, above all the
43
+ * detail card's access tab — which names the route it describes, so the reader
44
+ * knows what they are in for before they open it.
45
+ *
46
+ * `displayName` is written to stand alone in prose and routinely will not fit:
47
+ * a name ending in "Bot" or "Portal" repeats what the context already says,
48
+ * and the longest in use runs past thirty characters. Falls back to
49
+ * `displayName` when unset, so this is only worth setting where that reads
50
+ * badly in a tab.
51
+ */
52
+ shortName?: string;
41
53
  /**
42
54
  * Optionally - older name of approval method if there were migration in organization.
43
55
  */
@@ -1 +1 @@
1
- {"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;EACA,WAAA;AAAA;AAMF;;;AAAA,UAAiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;;;;UAMe,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EANc;;AAKhB;EAKE,WAAA;;;;EAIA,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;UAWG,OAAA;EAhBX;;;EAoBJ,WAAA;EAfI;;;EAmBJ,WAAA;EARe;;;EAYf,UAAA;AAAA;;;;UAMe,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;AAMF;UAAiB,gBAAA;;;;EAIf,kBAAA;EASA;;;EAJA,QAAA;EAmBA;;;EAfA,aAAA;EAmBkB;AAIpB;;EAnBE,wBAAA;EAmB0B;;AAG5B;EAjBE,KAAA,GAAQ,OAAA;;;;;EAMR,aAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA;;KAIG,aAAA,GAAgB,gBAAA;;KAGhB,IAAA,GAAO,OAAA"}
1
+ {"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;EACA,WAAA;AAAA;AAMF;;;AAAA,UAAiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;;;;UAMe,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EANc;;AAKhB;EAKE,WAAA;;;;;;;;;;;;EAYA,SAAA;EAAA;;;EAIA,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;AAWd;;UAAiB,OAAA;EAAO;;;EAItB,WAAA;EAQU;;AAMZ;EAVE,WAAA;;;;EAIA,UAAA;AAAA;;;;UAMe,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;;UAMe,gBAAA;EAgCG;;AAIpB;EAhCE,kBAAA;;;;EAKA,QAAA;EA8Bc;;;EA1Bd,aAAA;;;;EAIA,wBAAA;;;;EAKA,KAAA,GAAQ,OAAA;;;;;EAMR,aAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA;;KAIG,aAAA,GAAgB,gBAAA;;KAGhB,IAAA,GAAO,OAAA"}