@pithy-sh/support 0.1.1 → 0.1.3
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/ai/classify.d.ts +90 -0
- package/dist/ai/classify.d.ts.map +1 -0
- package/dist/ai/classify.js +151 -0
- package/dist/attachment/store.d.ts +53 -0
- package/dist/attachment/store.d.ts.map +1 -0
- package/dist/attachment/store.js +67 -0
- package/dist/audit/actions.d.ts +68 -0
- package/dist/audit/actions.d.ts.map +1 -0
- package/dist/audit/actions.js +68 -0
- package/dist/capability.d.ts +93 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +187 -0
- package/dist/client/projection.d.ts +56 -0
- package/dist/client/projection.d.ts.map +1 -0
- package/dist/client/projection.js +0 -0
- package/dist/config/config.d.ts +215 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +169 -0
- package/dist/data/attachment.d.ts +30 -0
- package/dist/data/attachment.d.ts.map +1 -0
- package/dist/data/attachment.js +32 -0
- package/dist/data/billingScope.d.ts +30 -0
- package/dist/data/billingScope.d.ts.map +1 -0
- package/dist/data/billingScope.js +32 -0
- package/dist/data/categories.d.ts +54 -0
- package/dist/data/categories.d.ts.map +1 -0
- package/dist/data/categories.js +97 -0
- package/dist/data/classification.d.ts +34 -0
- package/dist/data/classification.d.ts.map +1 -0
- package/dist/data/classification.js +28 -0
- package/dist/data/enums.d.ts +82 -0
- package/dist/data/enums.d.ts.map +1 -0
- package/dist/data/enums.js +70 -0
- package/dist/data/flag.d.ts +25 -0
- package/dist/data/flag.d.ts.map +1 -0
- package/dist/data/flag.js +27 -0
- package/dist/data/message.d.ts +81 -0
- package/dist/data/message.d.ts.map +1 -0
- package/dist/data/message.js +84 -0
- package/dist/data/tables.d.ts +160 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tables.js +44 -0
- package/dist/data/thread.d.ts +77 -0
- package/dist/data/thread.d.ts.map +1 -0
- package/dist/data/thread.js +65 -0
- package/dist/error/errors.d.ts +64 -0
- package/dist/error/errors.d.ts.map +1 -0
- package/dist/error/errors.js +82 -0
- package/dist/http/guards.d.ts +44 -0
- package/dist/http/guards.d.ts.map +1 -0
- package/dist/http/guards.js +53 -0
- package/dist/http/handlers.d.ts +89 -0
- package/dist/http/handlers.d.ts.map +1 -0
- package/dist/http/handlers.js +248 -0
- package/dist/http/resolve.d.ts +44 -0
- package/dist/http/resolve.d.ts.map +1 -0
- package/dist/http/resolve.js +72 -0
- package/dist/http/responses.d.ts +829 -0
- package/dist/http/responses.d.ts.map +1 -0
- package/dist/http/responses.js +303 -0
- package/dist/http/routes.d.ts +109 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +62 -0
- package/dist/http/schemas.d.ts +121 -0
- package/dist/http/schemas.d.ts.map +1 -0
- package/dist/http/schemas.js +89 -0
- package/dist/http/scopes.d.ts +56 -0
- package/dist/http/scopes.d.ts.map +1 -0
- package/dist/http/scopes.js +107 -0
- package/dist/http/views.d.ts +36 -0
- package/dist/http/views.d.ts.map +1 -0
- package/dist/http/views.js +144 -0
- package/dist/inbound/authenticity.d.ts +81 -0
- package/dist/inbound/authenticity.d.ts.map +1 -0
- package/dist/inbound/authenticity.js +61 -0
- package/dist/inbound/guard.d.ts +74 -0
- package/dist/inbound/guard.d.ts.map +1 -0
- package/dist/inbound/guard.js +78 -0
- package/dist/inbound/handler.d.ts +28 -0
- package/dist/inbound/handler.d.ts.map +1 -0
- package/dist/inbound/handler.js +82 -0
- package/dist/inbound/ingest.d.ts +104 -0
- package/dist/inbound/ingest.d.ts.map +1 -0
- package/dist/inbound/ingest.js +326 -0
- package/dist/inbound/recipient.d.ts +16 -0
- package/dist/inbound/recipient.d.ts.map +1 -0
- package/dist/inbound/recipient.js +46 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/link/sender.d.ts +160 -0
- package/dist/link/sender.d.ts.map +1 -0
- package/dist/link/sender.js +200 -0
- package/dist/migrations/0001_threads.d.ts +26 -0
- package/dist/migrations/0001_threads.d.ts.map +1 -0
- package/dist/migrations/0001_threads.js +103 -0
- package/dist/mime/address.d.ts +22 -0
- package/dist/mime/address.d.ts.map +1 -0
- package/dist/mime/address.js +28 -0
- package/dist/mime/parse.d.ts +96 -0
- package/dist/mime/parse.d.ts.map +1 -0
- package/dist/mime/parse.js +146 -0
- package/dist/mime/sanitize.d.ts +25 -0
- package/dist/mime/sanitize.d.ts.map +1 -0
- package/dist/mime/sanitize.js +212 -0
- package/dist/mime/threading.d.ts +79 -0
- package/dist/mime/threading.d.ts.map +1 -0
- package/dist/mime/threading.js +111 -0
- package/dist/mime/truncate.d.ts +17 -0
- package/dist/mime/truncate.d.ts.map +1 -0
- package/dist/mime/truncate.js +47 -0
- package/dist/provision/provisionSupport.d.ts +158 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -0
- package/dist/provision/provisionSupport.js +110 -0
- package/dist/provision/resolveSupportConfig.d.ts +45 -0
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -0
- package/dist/provision/resolveSupportConfig.js +22 -0
- package/dist/reply/send.d.ts +123 -0
- package/dist/reply/send.d.ts.map +1 -0
- package/dist/reply/send.js +153 -0
- package/dist/reply/snippets.d.ts +79 -0
- package/dist/reply/snippets.d.ts.map +1 -0
- package/dist/reply/snippets.js +135 -0
- package/dist/secret/registry.d.ts +18 -0
- package/dist/secret/registry.d.ts.map +1 -0
- package/dist/secret/registry.js +21 -0
- package/dist/seeds/example.d.ts +3 -0
- package/dist/seeds/example.d.ts.map +1 -0
- package/dist/seeds/example.js +350 -0
- package/dist/store/paging.d.ts +18 -0
- package/dist/store/paging.d.ts.map +1 -0
- package/dist/store/paging.js +20 -0
- package/dist/store/search.d.ts +67 -0
- package/dist/store/search.d.ts.map +1 -0
- package/dist/store/search.js +160 -0
- package/dist/store/searchIndex.d.ts +53 -0
- package/dist/store/searchIndex.d.ts.map +1 -0
- package/dist/store/searchIndex.js +66 -0
- package/dist/store/threads.d.ts +147 -0
- package/dist/store/threads.d.ts.map +1 -0
- package/dist/store/threads.js +197 -0
- package/dist/submission/encoding.d.ts +21 -0
- package/dist/submission/encoding.d.ts.map +1 -0
- package/dist/submission/encoding.js +55 -0
- package/dist/submission/guard.d.ts +71 -0
- package/dist/submission/guard.d.ts.map +1 -0
- package/dist/submission/guard.js +46 -0
- package/dist/submission/submit.d.ts +152 -0
- package/dist/submission/submit.d.ts.map +1 -0
- package/dist/submission/submit.js +319 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/workflows/classify.d.ts +63 -0
- package/dist/workflows/classify.d.ts.map +1 -0
- package/dist/workflows/classify.js +68 -0
- package/dist/workflows/retryPolicy.d.ts +39 -0
- package/dist/workflows/retryPolicy.d.ts.map +1 -0
- package/dist/workflows/retryPolicy.js +43 -0
- package/dist/workflows/specs.d.ts +43 -0
- package/dist/workflows/specs.d.ts.map +1 -0
- package/dist/workflows/specs.js +46 -0
- package/dist/workflows/worker.d.ts +48 -0
- package/dist/workflows/worker.d.ts.map +1 -0
- package/dist/workflows/worker.js +37 -0
- package/package.json +24 -15
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { SupportClassificationError, SupportNotFoundError } from "../error/errors.js";
|
|
2
|
+
import { repliesForCategory } from "../reply/snippets.js";
|
|
3
|
+
import { attachmentUrl } from "../attachment/store.js";
|
|
4
|
+
import { SupportAuditActions } from "../audit/actions.js";
|
|
5
|
+
import { resolveSenderContext, resolveSubmitterAccount, resolveSubmitterContext } from "../link/sender.js";
|
|
6
|
+
import { sendReply } from "../reply/send.js";
|
|
7
|
+
import { listOwnThreads, listThreads, readOwnThread, readThread, setArchived, setFlags } from "../store/threads.js";
|
|
8
|
+
import { decodeBase64 } from "../submission/encoding.js";
|
|
9
|
+
import { submitFeedback } from "../submission/submit.js";
|
|
10
|
+
import { latestInboundMessageId } from "../workflows/classify.js";
|
|
11
|
+
import { listedThreadView, messageView, myMessageView, myThreadView, senderView, threadView } from "./views.js";
|
|
12
|
+
import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
|
|
13
|
+
//#region src/http/handlers.ts
|
|
14
|
+
/** List the inbox. */
|
|
15
|
+
async function listInbox(deps, query, viewer) {
|
|
16
|
+
const page = await listThreads(deps.db, {
|
|
17
|
+
archived: query.archived === "true",
|
|
18
|
+
category: query.category,
|
|
19
|
+
declaredCategory: query.declaredCategory,
|
|
20
|
+
priority: query.priority,
|
|
21
|
+
sentiment: query.sentiment,
|
|
22
|
+
channel: query.channel,
|
|
23
|
+
inbox: query.inbox,
|
|
24
|
+
q: query.q,
|
|
25
|
+
cursor: query.cursor,
|
|
26
|
+
limit: query.limit
|
|
27
|
+
}, {
|
|
28
|
+
fts: deps.fts,
|
|
29
|
+
viewer,
|
|
30
|
+
now: deps.now(),
|
|
31
|
+
log: deps.log
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
threads: page.threads.map(listedThreadView),
|
|
35
|
+
nextCursor: page.nextCursor
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Present an attachment.
|
|
40
|
+
*
|
|
41
|
+
* The `storageKey` is **never** in the response. It is server-derived and opaque precisely so a
|
|
42
|
+
* client cannot name an object or guess the one beside it, and echoing it back would hand that
|
|
43
|
+
* capability away for no benefit — a client cannot use a key for anything except guessing.
|
|
44
|
+
*/
|
|
45
|
+
async function presentAttachments(deps, attachments) {
|
|
46
|
+
return Promise.all(attachments.map(async (attachment) => {
|
|
47
|
+
let url = null;
|
|
48
|
+
if (deps.store) try {
|
|
49
|
+
url = await attachmentUrl(deps.store, attachment.storageKey);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
deps.log.warn("support attachment URL not signed", {
|
|
52
|
+
attachmentId: attachment.id,
|
|
53
|
+
error
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
id: attachment.id,
|
|
58
|
+
filename: attachment.filename,
|
|
59
|
+
contentType: attachment.contentType,
|
|
60
|
+
size: attachment.size,
|
|
61
|
+
sha256: attachment.sha256,
|
|
62
|
+
inline: attachment.inline,
|
|
63
|
+
url
|
|
64
|
+
};
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
/** Read one conversation in full. */
|
|
68
|
+
async function readConversation(deps, threadId) {
|
|
69
|
+
const detail = await readThread(deps.db, threadId);
|
|
70
|
+
const [attachments, sender] = await Promise.all([presentAttachments(deps, detail.attachments), detail.thread.accountLinkSource === "session" && detail.thread.userId ? resolveSubmitterContext(deps.d1, detail.thread.userId, deps.now()) : resolveSenderContext(deps.d1, detail.thread.fromAddress, deps.now(), { authenticated: detail.thread.senderAuthenticated })]);
|
|
71
|
+
return {
|
|
72
|
+
thread: threadView(detail.thread),
|
|
73
|
+
messages: detail.messages.map(messageView),
|
|
74
|
+
attachments,
|
|
75
|
+
sender: senderView(sender),
|
|
76
|
+
replies: repliesForCategory(deps.snippets, detail.thread.category)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Mark a conversation done, or reopen it. */
|
|
80
|
+
async function archiveConversation(deps, threadId, input, viewer) {
|
|
81
|
+
const thread = await setArchived(deps.db, threadId, input.archived, viewer, deps.now());
|
|
82
|
+
await deps.emit({
|
|
83
|
+
action: input.archived ? SupportAuditActions.threadArchived : SupportAuditActions.threadUnarchived,
|
|
84
|
+
outcome: "success",
|
|
85
|
+
actorType: "control-plane",
|
|
86
|
+
actorId: viewer,
|
|
87
|
+
resourceType: "support_thread",
|
|
88
|
+
resourceId: threadId,
|
|
89
|
+
metadata: { archived: input.archived }
|
|
90
|
+
});
|
|
91
|
+
return threadView(thread);
|
|
92
|
+
}
|
|
93
|
+
/** Answer the customer. */
|
|
94
|
+
async function replyToConversation(deps, threadId, input, viewer) {
|
|
95
|
+
return sendReply({
|
|
96
|
+
db: deps.db,
|
|
97
|
+
config: deps.config,
|
|
98
|
+
enqueue: deps.enqueue,
|
|
99
|
+
fts: deps.fts,
|
|
100
|
+
emit: deps.emit,
|
|
101
|
+
log: deps.log,
|
|
102
|
+
newId: deps.newId,
|
|
103
|
+
now: deps.now
|
|
104
|
+
}, {
|
|
105
|
+
threadId,
|
|
106
|
+
body: input.body,
|
|
107
|
+
agentName: input.agentName,
|
|
108
|
+
viewer
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** Re-run the classifier over a conversation's latest inbound message. */
|
|
112
|
+
async function reclassifyConversation(deps, threadId, viewer) {
|
|
113
|
+
const messageId = await latestInboundMessageId(deps.db, threadId);
|
|
114
|
+
if (!messageId) throw new SupportNotFoundError({
|
|
115
|
+
message: "That conversation has nothing to classify.",
|
|
116
|
+
detail: `thread ${threadId} has no inbound message`
|
|
117
|
+
});
|
|
118
|
+
if (!await deps.dispatchClassify(messageId)) throw new SupportClassificationError({
|
|
119
|
+
message: "Classification is not available on this deployment.",
|
|
120
|
+
action: "Run `pithy support provision` to deploy the classification worker, then retry.",
|
|
121
|
+
detail: `the SUPPORT_CLASSIFY workflow binding is absent, so no instance was started`
|
|
122
|
+
});
|
|
123
|
+
await deps.emit({
|
|
124
|
+
action: SupportAuditActions.threadReclassified,
|
|
125
|
+
outcome: "success",
|
|
126
|
+
actorType: "control-plane",
|
|
127
|
+
actorId: viewer,
|
|
128
|
+
resourceType: "support_thread",
|
|
129
|
+
resourceId: threadId,
|
|
130
|
+
metadata: { messageId }
|
|
131
|
+
});
|
|
132
|
+
return { messageId };
|
|
133
|
+
}
|
|
134
|
+
/** Set one viewer's private flags. */
|
|
135
|
+
async function updateFlags(deps, threadId, input, viewer) {
|
|
136
|
+
await setFlags(deps.db, {
|
|
137
|
+
threadId,
|
|
138
|
+
viewer,
|
|
139
|
+
read: input.read,
|
|
140
|
+
snoozedUntil: input.snoozedUntil === void 0 ? void 0 : input.snoozedUntil ? new Date(input.snoozedUntil) : null,
|
|
141
|
+
newId: deps.newId,
|
|
142
|
+
now: deps.now()
|
|
143
|
+
});
|
|
144
|
+
return { ok: true };
|
|
145
|
+
}
|
|
146
|
+
/** The canned reply catalog. */
|
|
147
|
+
function listReplies(deps, query) {
|
|
148
|
+
return repliesForCategory(deps.snippets, query.category ?? "");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Take an in-app support request from a signed-in user.
|
|
152
|
+
*
|
|
153
|
+
* `userId` is a parameter rather than something read out of `input`, and that is the contract this
|
|
154
|
+
* whole channel rests on: it comes from `c.var.auth`, which `requireAuth()` filled before the route's
|
|
155
|
+
* validator ran. Nothing in the submitted body names an account, and the schema has no field that
|
|
156
|
+
* could.
|
|
157
|
+
*
|
|
158
|
+
* The configured length bounds are applied here rather than in the schema, for the reason
|
|
159
|
+
* `schemas.ts` states: a request schema is built once at module load and the adopter's numbers are
|
|
160
|
+
* resolved per project. This is config-backed resolution, which belongs in a handler — the same rule
|
|
161
|
+
* that keeps `board()` and `currency()` lookups out of a param schema.
|
|
162
|
+
*/
|
|
163
|
+
async function submitFeedbackRequest(deps, input, userId) {
|
|
164
|
+
const bounds = deps.config.submission;
|
|
165
|
+
if (input.subject.length > bounds.maxSubjectChars) throw new ValidationError({
|
|
166
|
+
message: `A subject may be at most ${bounds.maxSubjectChars} characters.`,
|
|
167
|
+
action: "Shorten the subject and try again.",
|
|
168
|
+
detail: `submitted subject is ${input.subject.length} characters, over the ${bounds.maxSubjectChars} bound`
|
|
169
|
+
});
|
|
170
|
+
if (input.body.length > bounds.maxBodyChars) throw new ValidationError({
|
|
171
|
+
message: `A support request may be at most ${bounds.maxBodyChars} characters.`,
|
|
172
|
+
action: "Shorten the message and try again, or attach the detail as a file.",
|
|
173
|
+
detail: `submitted body is ${input.body.length} characters, over the ${bounds.maxBodyChars} bound`
|
|
174
|
+
});
|
|
175
|
+
if (input.attachments.length > bounds.attachments.maxCount) throw new ValidationError({
|
|
176
|
+
message: `A support request may carry at most ${bounds.attachments.maxCount} attachments.`,
|
|
177
|
+
action: `Send at most ${bounds.attachments.maxCount} files.`,
|
|
178
|
+
detail: `submission declared ${input.attachments.length} attachments, over the ${bounds.attachments.maxCount} bound`
|
|
179
|
+
});
|
|
180
|
+
const outcome = await submitFeedback({
|
|
181
|
+
db: deps.db,
|
|
182
|
+
config: deps.config,
|
|
183
|
+
categories: deps.categories,
|
|
184
|
+
bucket: deps.bucket,
|
|
185
|
+
fts: deps.fts,
|
|
186
|
+
resolveAccount: (id) => resolveSubmitterAccount(deps.d1, id),
|
|
187
|
+
dispatchClassify: deps.dispatchClassify,
|
|
188
|
+
emit: deps.emit,
|
|
189
|
+
log: deps.log,
|
|
190
|
+
newId: deps.newId,
|
|
191
|
+
now: deps.now
|
|
192
|
+
}, {
|
|
193
|
+
userId,
|
|
194
|
+
subject: input.subject,
|
|
195
|
+
body: input.body,
|
|
196
|
+
declaredCategory: input.declaredCategory,
|
|
197
|
+
threadId: input.threadId,
|
|
198
|
+
context: input.context,
|
|
199
|
+
attachments: input.attachments.map((attachment) => ({
|
|
200
|
+
filename: attachment.filename,
|
|
201
|
+
contentType: attachment.contentType,
|
|
202
|
+
bytes: decodeBase64(attachment.data, { maxBytes: bounds.attachments.maxBytes })
|
|
203
|
+
}))
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
threadId: outcome.threadId,
|
|
207
|
+
messageId: outcome.messageId,
|
|
208
|
+
opened: outcome.newThread,
|
|
209
|
+
attachments: outcome.attachments
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** List the caller's own in-app conversations. */
|
|
213
|
+
async function listMyThreads(deps, query, userId) {
|
|
214
|
+
const page = await listOwnThreads(deps.db, userId, {
|
|
215
|
+
cursor: query.cursor,
|
|
216
|
+
limit: query.limit
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
threads: page.threads.map(myThreadView),
|
|
220
|
+
nextCursor: page.nextCursor
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Read one of the caller's own conversations.
|
|
225
|
+
*
|
|
226
|
+
* The scoping is `readOwnThread`'s `where`, so a thread belonging to somebody else raises the same
|
|
227
|
+
* `support/not_found` as one that never existed. The projection is `myThreadView`/`myMessageView`,
|
|
228
|
+
* built from scratch rather than by omitting fields from the operator's — see `views.ts` for why that
|
|
229
|
+
* distinction is the security boundary rather than a style preference.
|
|
230
|
+
*/
|
|
231
|
+
async function readMyThread(deps, threadId, userId) {
|
|
232
|
+
const detail = await readOwnThread(deps.db, threadId, userId);
|
|
233
|
+
const attachments = await presentAttachments(deps, detail.attachments);
|
|
234
|
+
const byMessage = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const [index, attachment] of detail.attachments.entries()) {
|
|
236
|
+
const view = attachments[index];
|
|
237
|
+
if (!view) continue;
|
|
238
|
+
const bucket = byMessage.get(attachment.messageId);
|
|
239
|
+
if (bucket) bucket.push(view);
|
|
240
|
+
else byMessage.set(attachment.messageId, [view]);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
thread: myThreadView(detail.thread),
|
|
244
|
+
messages: detail.messages.map((message) => myMessageView(message, byMessage.get(message.id) ?? []))
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { archiveConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, submitFeedbackRequest, updateFlags };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { D1Database, R2Bucket } from "@cloudflare/workers-types";
|
|
2
|
+
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
3
|
+
import type { Logger } from "@pithy-sh/core/src/logger/logger";
|
|
4
|
+
import type { SecretsStoreEnv } from "@pithy-sh/secrets/src/env/bindings";
|
|
5
|
+
import { objectStore } from "@pithy-sh/storage/src/object/store";
|
|
6
|
+
import type { Context } from "hono";
|
|
7
|
+
import type { SupportWiring } from "../capability";
|
|
8
|
+
import type { HandlerDeps } from "./handlers";
|
|
9
|
+
/**
|
|
10
|
+
* Turning a Worker env into the dependencies the handlers take.
|
|
11
|
+
*
|
|
12
|
+
* The pattern every capability uses: nothing below `resolve.ts` ever reads a binding off `env`, so
|
|
13
|
+
* the handlers, the store, the reply path, and the ingest orchestration are all testable by handing
|
|
14
|
+
* them objects. This is the one file that knows what `env.DB` is called.
|
|
15
|
+
*/
|
|
16
|
+
/** The bindings support reads. */
|
|
17
|
+
export interface SupportEnv extends SecretsStoreEnv {
|
|
18
|
+
/** The app database every support table lives in. */
|
|
19
|
+
DB: D1Database;
|
|
20
|
+
/** Attachment and raw-message bytes. Optional — an inbox with attachments off never touches it. */
|
|
21
|
+
SUPPORT_BUCKET?: R2Bucket;
|
|
22
|
+
}
|
|
23
|
+
/** Read the `DB` binding, or fail loudly rather than as a silent undefined three layers down. */
|
|
24
|
+
export declare function resolveDb(env: Record<string, unknown>): D1Database;
|
|
25
|
+
/**
|
|
26
|
+
* Start a classification.
|
|
27
|
+
*
|
|
28
|
+
* Dispatched through the registry rather than by reaching for the binding directly, so the params
|
|
29
|
+
* are validated at the call site and a missing binding degrades to a logged skip — which is the
|
|
30
|
+
* whole reason the job is declared `optional`. A project that has not run `pithy support provision`
|
|
31
|
+
* still receives mail; its threads simply stay `uncategorized`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function makeClassifyDispatcher(env: Record<string, unknown>, log: Logger): (messageId: string) => Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Build the object store attachment URLs are signed through.
|
|
36
|
+
*
|
|
37
|
+
* Returns undefined rather than throwing when the bucket is unbound: a deployment with attachments
|
|
38
|
+
* off has no bucket by design, and a thread view must still render. Credentials resolve lazily
|
|
39
|
+
* inside the seam, so this costs nothing until something is actually signed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveStore(env: SupportEnv): ReturnType<typeof objectStore> | undefined;
|
|
42
|
+
/** Build the per-request handler dependencies. */
|
|
43
|
+
export declare function makeResolveDeps(wiring: SupportWiring): (c: Context<PithyHonoEnv>) => Promise<HandlerDeps>;
|
|
44
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/http/resolve.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;GAMG;AAEH,kCAAkC;AAClC,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,qDAAqD;IACrD,EAAE,EAAE,UAAU,CAAC;IACf,mGAAmG;IACnG,cAAc,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAED,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAUlE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,MAAM,GACV,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAUzC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,SAAS,CAGxF;AAED,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CA2BzG"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { supportDatabase } from "../data/tables.js";
|
|
2
|
+
import { SUPPORT_R2_SECRET } from "../secret/registry.js";
|
|
3
|
+
import { supportWorkflowRegistry, supportWorkflows } from "../workflows/specs.js";
|
|
4
|
+
import { InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
5
|
+
import { objectStore } from "@pithy-sh/storage/src/object/store";
|
|
6
|
+
//#region src/http/resolve.ts
|
|
7
|
+
/** Read the `DB` binding, or fail loudly rather than as a silent undefined three layers down. */
|
|
8
|
+
function resolveDb(env) {
|
|
9
|
+
const binding = env.DB;
|
|
10
|
+
if (!binding) throw new InternalError({
|
|
11
|
+
message: "The support database binding is missing.",
|
|
12
|
+
action: "Add the `DB` D1 binding to this Worker's wrangler.jsonc, then redeploy.",
|
|
13
|
+
detail: "D1 binding \"DB\" is not present on the worker env"
|
|
14
|
+
});
|
|
15
|
+
return binding;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Start a classification.
|
|
19
|
+
*
|
|
20
|
+
* Dispatched through the registry rather than by reaching for the binding directly, so the params
|
|
21
|
+
* are validated at the call site and a missing binding degrades to a logged skip — which is the
|
|
22
|
+
* whole reason the job is declared `optional`. A project that has not run `pithy support provision`
|
|
23
|
+
* still receives mail; its threads simply stay `uncategorized`.
|
|
24
|
+
*/
|
|
25
|
+
function makeClassifyDispatcher(env, log) {
|
|
26
|
+
return async (messageId) => {
|
|
27
|
+
const { triggerWorkflow } = await import("@pithy-sh/core/src/workflow/dispatch");
|
|
28
|
+
await triggerWorkflow(env, supportWorkflowRegistry, "support/classify", { messageId }, log);
|
|
29
|
+
return env[supportWorkflows.classify.binding] != null;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the object store attachment URLs are signed through.
|
|
34
|
+
*
|
|
35
|
+
* Returns undefined rather than throwing when the bucket is unbound: a deployment with attachments
|
|
36
|
+
* off has no bucket by design, and a thread view must still render. Credentials resolve lazily
|
|
37
|
+
* inside the seam, so this costs nothing until something is actually signed.
|
|
38
|
+
*/
|
|
39
|
+
function resolveStore(env) {
|
|
40
|
+
if (!env.SUPPORT_BUCKET) return void 0;
|
|
41
|
+
return objectStore({
|
|
42
|
+
bucket: env.SUPPORT_BUCKET,
|
|
43
|
+
env,
|
|
44
|
+
secretName: SUPPORT_R2_SECRET
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Build the per-request handler dependencies. */
|
|
48
|
+
function makeResolveDeps(wiring) {
|
|
49
|
+
return async (c) => {
|
|
50
|
+
const env = c.env;
|
|
51
|
+
const d1 = resolveDb(env);
|
|
52
|
+
const log = c.var.log.child("support");
|
|
53
|
+
return {
|
|
54
|
+
db: supportDatabase(d1),
|
|
55
|
+
d1,
|
|
56
|
+
config: wiring.config,
|
|
57
|
+
categories: wiring.categories,
|
|
58
|
+
snippets: wiring.snippets,
|
|
59
|
+
fts: wiring.config.search.fts,
|
|
60
|
+
bucket: env.SUPPORT_BUCKET,
|
|
61
|
+
store: resolveStore(env),
|
|
62
|
+
enqueue: wiring.enqueueEmail ? (input) => wiring.enqueueEmail?.(env, input) : void 0,
|
|
63
|
+
dispatchClassify: makeClassifyDispatcher(env, log),
|
|
64
|
+
emit: c.var.emit,
|
|
65
|
+
log,
|
|
66
|
+
newId: () => crypto.randomUUID(),
|
|
67
|
+
now: () => /* @__PURE__ */ new Date()
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { makeClassifyDispatcher, makeResolveDeps, resolveDb, resolveStore };
|