@igstack/app-catalog-backend-core 6.3.1-alpha-20261003051755 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/syncAppCatalog.mjs +1 -0
- package/dist/db/syncAppCatalog.mjs.map +1 -1
- package/dist/generated/prisma/client.mjs.map +1 -1
- package/dist/generated/prisma/commonInputTypes.d.mts +72 -72
- package/dist/generated/prisma/commonInputTypes.d.mts.map +1 -1
- package/dist/generated/prisma/internal/class.d.mts +17 -5
- package/dist/generated/prisma/internal/class.d.mts.map +1 -1
- package/dist/generated/prisma/internal/class.mjs +4 -4
- package/dist/generated/prisma/internal/class.mjs.map +1 -1
- package/dist/generated/prisma/internal/prismaNamespace.d.mts +145 -51
- package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
- package/dist/generated/prisma/models/DbApprovalMethod.d.mts +30 -3
- package/dist/generated/prisma/models/DbApprovalMethod.d.mts.map +1 -1
- package/dist/generated/prisma/models/DbAsset.d.mts +2 -11
- package/dist/generated/prisma/models/DbAsset.d.mts.map +1 -1
- package/dist/generated/prisma/models/DbFeedback.d.mts +1565 -0
- package/dist/generated/prisma/models/DbFeedback.d.mts.map +1 -0
- package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts +1431 -0
- package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts.map +1 -0
- package/dist/generated/prisma/models/DbResource.d.mts +108 -60
- package/dist/generated/prisma/models/DbResource.d.mts.map +1 -1
- package/dist/generated/prisma/models/SourceReference.d.mts +1 -1
- package/dist/generated/prisma/models.d.mts +4 -3
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +5 -3
- package/dist/middleware/createAcMiddleware.mjs +2 -2
- package/dist/middleware/createAcMiddleware.mjs.map +1 -1
- package/dist/middleware/featureRegistry.mjs +3 -1
- package/dist/middleware/featureRegistry.mjs.map +1 -1
- package/dist/modules/appCatalog/service.d.mts.map +1 -1
- package/dist/modules/appCatalog/service.mjs +1 -0
- package/dist/modules/appCatalog/service.mjs.map +1 -1
- package/dist/modules/feedback/feedbackAttachmentController.d.mts +22 -0
- package/dist/modules/feedback/feedbackAttachmentController.d.mts.map +1 -0
- package/dist/modules/feedback/feedbackAttachmentController.mjs +192 -0
- package/dist/modules/feedback/feedbackAttachmentController.mjs.map +1 -0
- package/dist/modules/feedback/feedbackRouter.d.mts +54 -0
- package/dist/modules/feedback/feedbackRouter.d.mts.map +1 -0
- package/dist/modules/feedback/feedbackRouter.mjs +59 -0
- package/dist/modules/feedback/feedbackRouter.mjs.map +1 -0
- package/dist/modules/feedback/service.d.mts +42 -0
- package/dist/modules/feedback/service.d.mts.map +1 -0
- package/dist/modules/feedback/service.mjs +165 -0
- package/dist/modules/feedback/service.mjs.map +1 -0
- package/dist/modules/visitor/visitorIdentity.d.mts +86 -0
- package/dist/modules/visitor/visitorIdentity.d.mts.map +1 -0
- package/dist/modules/{comments → visitor}/visitorIdentity.mjs +53 -2
- package/dist/modules/visitor/visitorIdentity.mjs.map +1 -0
- package/dist/server/acTrpcContext.d.mts +8 -7
- package/dist/server/acTrpcContext.d.mts.map +1 -1
- package/dist/server/acTrpcContext.mjs +2 -2
- package/dist/server/acTrpcContext.mjs.map +1 -1
- package/dist/server/controller.d.mts +15 -8
- package/dist/server/controller.d.mts.map +1 -1
- package/dist/server/controller.mjs +2 -2
- package/dist/server/controller.mjs.map +1 -1
- package/dist/types/common/appCatalogTypes.d.mts +12 -0
- package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
- package/dist/types/common/approvalMethodTypes.d.mts +12 -0
- package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
- package/package.json +5 -5
- package/prisma/migrations/20261004000000_feedback_replaces_comments/migration.sql +74 -0
- package/prisma/schema.prisma +84 -24
- package/src/__tests__/feedbackAttachmentUpload.test.ts +192 -0
- package/src/__tests__/{comments.test.ts → visitorIdentity.test.ts} +1 -1
- package/src/db/syncAppCatalog.ts +1 -0
- package/src/generated/prisma/browser.ts +22 -7
- package/src/generated/prisma/client.ts +22 -7
- package/src/generated/prisma/commonInputTypes.ts +85 -85
- package/src/generated/prisma/internal/class.ts +18 -8
- package/src/generated/prisma/internal/prismaNamespace.ts +143 -47
- package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +24 -3
- package/src/generated/prisma/models/DbApprovalMethod.ts +29 -1
- package/src/generated/prisma/models/DbAsset.ts +0 -12
- package/src/generated/prisma/models/DbFeedback.ts +1814 -0
- package/src/generated/prisma/models/DbFeedbackAttachment.ts +1661 -0
- package/src/generated/prisma/models/DbResource.ts +107 -58
- package/src/generated/prisma/models.ts +2 -1
- package/src/index.ts +24 -4
- package/src/middleware/createAcMiddleware.ts +4 -3
- package/src/middleware/featureRegistry.ts +7 -0
- package/src/modules/appCatalog/service.ts +1 -0
- package/src/modules/feedback/feedbackAttachmentController.ts +248 -0
- package/src/modules/feedback/feedbackRouter.ts +102 -0
- package/src/modules/feedback/service.ts +295 -0
- package/src/modules/{comments → visitor}/visitorIdentity.ts +84 -0
- package/src/server/acTrpcContext.ts +9 -8
- package/src/server/controller.ts +2 -2
- package/src/types/common/appCatalogTypes.ts +12 -0
- package/src/types/common/approvalMethodTypes.ts +12 -0
- package/dist/generated/prisma/models/DbComment.d.mts +0 -1340
- package/dist/generated/prisma/models/DbComment.d.mts.map +0 -1
- package/dist/modules/comments/commentsRouter.d.mts +0 -47
- package/dist/modules/comments/commentsRouter.d.mts.map +0 -1
- package/dist/modules/comments/commentsRouter.mjs +0 -55
- package/dist/modules/comments/commentsRouter.mjs.map +0 -1
- package/dist/modules/comments/service.d.mts +0 -40
- package/dist/modules/comments/service.d.mts.map +0 -1
- package/dist/modules/comments/service.mjs +0 -88
- package/dist/modules/comments/service.mjs.map +0 -1
- package/dist/modules/comments/visitorIdentity.d.mts +0 -15
- package/dist/modules/comments/visitorIdentity.d.mts.map +0 -1
- package/dist/modules/comments/visitorIdentity.mjs.map +0 -1
- package/src/generated/prisma/models/DbComment.ts +0 -1561
- package/src/modules/comments/commentsRouter.ts +0 -85
- 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/
|
|
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 {
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
16
|
+
actor: Actor | null;
|
|
16
17
|
}
|
|
17
18
|
interface AcTrpcContextOptions {
|
|
18
19
|
companySpecificBackend: AppCatalogCompanySpecificBackend;
|
|
19
20
|
user?: User | null;
|
|
20
21
|
isAdmin?: boolean;
|
|
21
|
-
|
|
22
|
+
actor?: Actor | null;
|
|
22
23
|
}
|
|
23
24
|
declare function createAcTrpcContext({
|
|
24
25
|
companySpecificBackend,
|
|
25
26
|
user,
|
|
26
27
|
isAdmin,
|
|
27
|
-
|
|
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
|
|
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,
|
|
2
|
+
function createAcTrpcContext({ companySpecificBackend, user = null, isAdmin = false, actor = null }) {
|
|
3
3
|
return {
|
|
4
4
|
companySpecificBackend,
|
|
5
5
|
user,
|
|
6
6
|
isAdmin,
|
|
7
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
73
|
-
|
|
77
|
+
resourceSlug?: string | undefined;
|
|
78
|
+
subject?: string | undefined;
|
|
79
|
+
body?: string | undefined;
|
|
80
|
+
attachmentIds?: string[] | undefined;
|
|
74
81
|
};
|
|
75
|
-
output:
|
|
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:
|
|
90
|
+
output: FeedbackView;
|
|
84
91
|
meta: object;
|
|
85
92
|
}>;
|
|
86
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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;
|
|
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;EA2DA;;;;;;;;EAlDA,UAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;IAAU,GAAA;IAAa,KAAA;EAAA;EACvB,QAAA;EACA,aAAA;EACA,OAAA,cAAqB,eAAA;EACrB,UAAA;IAiFQ,0HA/EN,IAAA,iCA4EF;IA1EE,eAAA,WA4EF;IA1EE,OAAA;EAAA;EA2EsB;EAxExB,QAAA;EA2EG;EAzEH,QAAA;EAyEmB;EAvEnB,SAAA;EAwEO;EAtEP,KAAA,GAAQ,WAAA;EAsEN;EApEF,UAAA,GAAa,SAAA;EAoEP;;;;;;;;;AAGR;;EA3DE,aAAA;EA2D8B;EAvD9B,UAAA;EA4De;EA1Df,IAAA;;EAEA,OAAA;EAyDA;EAvDA,eAAA;EAyDA;EAvDA,aAAA;EAuDW;EArDX,cAAA;EAwD0B;EAtD1B,KAAA,GAAQ,MAAA;EAsDkB;;;;;;EA/C1B,SAAA,GAAY,SAAA;EAwDG;;;;;EAlDf,aAAA;EACA,cAAA;EACA,mBAAA;EAiDA;;;;;EA3CA,cAAA;EA6CyB;EA3CzB,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;EAmBA;EAjBA,GAAA;EAiByB;EAfzB,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"}
|
|
@@ -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
|
|
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"}
|