@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.
Files changed (167) hide show
  1. package/dist/ai/classify.d.ts +90 -0
  2. package/dist/ai/classify.d.ts.map +1 -0
  3. package/dist/ai/classify.js +151 -0
  4. package/dist/attachment/store.d.ts +53 -0
  5. package/dist/attachment/store.d.ts.map +1 -0
  6. package/dist/attachment/store.js +67 -0
  7. package/dist/audit/actions.d.ts +68 -0
  8. package/dist/audit/actions.d.ts.map +1 -0
  9. package/dist/audit/actions.js +68 -0
  10. package/dist/capability.d.ts +93 -0
  11. package/dist/capability.d.ts.map +1 -0
  12. package/dist/capability.js +187 -0
  13. package/dist/client/projection.d.ts +56 -0
  14. package/dist/client/projection.d.ts.map +1 -0
  15. package/dist/client/projection.js +0 -0
  16. package/dist/config/config.d.ts +215 -0
  17. package/dist/config/config.d.ts.map +1 -0
  18. package/dist/config/config.js +169 -0
  19. package/dist/data/attachment.d.ts +30 -0
  20. package/dist/data/attachment.d.ts.map +1 -0
  21. package/dist/data/attachment.js +32 -0
  22. package/dist/data/billingScope.d.ts +30 -0
  23. package/dist/data/billingScope.d.ts.map +1 -0
  24. package/dist/data/billingScope.js +32 -0
  25. package/dist/data/categories.d.ts +54 -0
  26. package/dist/data/categories.d.ts.map +1 -0
  27. package/dist/data/categories.js +97 -0
  28. package/dist/data/classification.d.ts +34 -0
  29. package/dist/data/classification.d.ts.map +1 -0
  30. package/dist/data/classification.js +28 -0
  31. package/dist/data/enums.d.ts +82 -0
  32. package/dist/data/enums.d.ts.map +1 -0
  33. package/dist/data/enums.js +70 -0
  34. package/dist/data/flag.d.ts +25 -0
  35. package/dist/data/flag.d.ts.map +1 -0
  36. package/dist/data/flag.js +27 -0
  37. package/dist/data/message.d.ts +81 -0
  38. package/dist/data/message.d.ts.map +1 -0
  39. package/dist/data/message.js +84 -0
  40. package/dist/data/tables.d.ts +160 -0
  41. package/dist/data/tables.d.ts.map +1 -0
  42. package/dist/data/tables.js +44 -0
  43. package/dist/data/thread.d.ts +77 -0
  44. package/dist/data/thread.d.ts.map +1 -0
  45. package/dist/data/thread.js +65 -0
  46. package/dist/error/errors.d.ts +64 -0
  47. package/dist/error/errors.d.ts.map +1 -0
  48. package/dist/error/errors.js +82 -0
  49. package/dist/http/guards.d.ts +44 -0
  50. package/dist/http/guards.d.ts.map +1 -0
  51. package/dist/http/guards.js +53 -0
  52. package/dist/http/handlers.d.ts +89 -0
  53. package/dist/http/handlers.d.ts.map +1 -0
  54. package/dist/http/handlers.js +248 -0
  55. package/dist/http/resolve.d.ts +44 -0
  56. package/dist/http/resolve.d.ts.map +1 -0
  57. package/dist/http/resolve.js +72 -0
  58. package/dist/http/responses.d.ts +829 -0
  59. package/dist/http/responses.d.ts.map +1 -0
  60. package/dist/http/responses.js +303 -0
  61. package/dist/http/routes.d.ts +109 -0
  62. package/dist/http/routes.d.ts.map +1 -0
  63. package/dist/http/routes.js +62 -0
  64. package/dist/http/schemas.d.ts +121 -0
  65. package/dist/http/schemas.d.ts.map +1 -0
  66. package/dist/http/schemas.js +89 -0
  67. package/dist/http/scopes.d.ts +56 -0
  68. package/dist/http/scopes.d.ts.map +1 -0
  69. package/dist/http/scopes.js +107 -0
  70. package/dist/http/views.d.ts +36 -0
  71. package/dist/http/views.d.ts.map +1 -0
  72. package/dist/http/views.js +144 -0
  73. package/dist/inbound/authenticity.d.ts +81 -0
  74. package/dist/inbound/authenticity.d.ts.map +1 -0
  75. package/dist/inbound/authenticity.js +61 -0
  76. package/dist/inbound/guard.d.ts +74 -0
  77. package/dist/inbound/guard.d.ts.map +1 -0
  78. package/dist/inbound/guard.js +78 -0
  79. package/dist/inbound/handler.d.ts +28 -0
  80. package/dist/inbound/handler.d.ts.map +1 -0
  81. package/dist/inbound/handler.js +82 -0
  82. package/dist/inbound/ingest.d.ts +104 -0
  83. package/dist/inbound/ingest.d.ts.map +1 -0
  84. package/dist/inbound/ingest.js +326 -0
  85. package/dist/inbound/recipient.d.ts +16 -0
  86. package/dist/inbound/recipient.d.ts.map +1 -0
  87. package/dist/inbound/recipient.js +46 -0
  88. package/dist/index.d.ts +20 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +14 -0
  91. package/dist/link/sender.d.ts +160 -0
  92. package/dist/link/sender.d.ts.map +1 -0
  93. package/dist/link/sender.js +200 -0
  94. package/dist/migrations/0001_threads.d.ts +26 -0
  95. package/dist/migrations/0001_threads.d.ts.map +1 -0
  96. package/dist/migrations/0001_threads.js +103 -0
  97. package/dist/mime/address.d.ts +22 -0
  98. package/dist/mime/address.d.ts.map +1 -0
  99. package/dist/mime/address.js +28 -0
  100. package/dist/mime/parse.d.ts +96 -0
  101. package/dist/mime/parse.d.ts.map +1 -0
  102. package/dist/mime/parse.js +146 -0
  103. package/dist/mime/sanitize.d.ts +25 -0
  104. package/dist/mime/sanitize.d.ts.map +1 -0
  105. package/dist/mime/sanitize.js +212 -0
  106. package/dist/mime/threading.d.ts +79 -0
  107. package/dist/mime/threading.d.ts.map +1 -0
  108. package/dist/mime/threading.js +111 -0
  109. package/dist/mime/truncate.d.ts +17 -0
  110. package/dist/mime/truncate.d.ts.map +1 -0
  111. package/dist/mime/truncate.js +47 -0
  112. package/dist/provision/provisionSupport.d.ts +158 -0
  113. package/dist/provision/provisionSupport.d.ts.map +1 -0
  114. package/dist/provision/provisionSupport.js +110 -0
  115. package/dist/provision/resolveSupportConfig.d.ts +45 -0
  116. package/dist/provision/resolveSupportConfig.d.ts.map +1 -0
  117. package/dist/provision/resolveSupportConfig.js +22 -0
  118. package/dist/reply/send.d.ts +123 -0
  119. package/dist/reply/send.d.ts.map +1 -0
  120. package/dist/reply/send.js +153 -0
  121. package/dist/reply/snippets.d.ts +79 -0
  122. package/dist/reply/snippets.d.ts.map +1 -0
  123. package/dist/reply/snippets.js +135 -0
  124. package/dist/secret/registry.d.ts +18 -0
  125. package/dist/secret/registry.d.ts.map +1 -0
  126. package/dist/secret/registry.js +21 -0
  127. package/dist/seeds/example.d.ts +3 -0
  128. package/dist/seeds/example.d.ts.map +1 -0
  129. package/dist/seeds/example.js +350 -0
  130. package/dist/store/paging.d.ts +18 -0
  131. package/dist/store/paging.d.ts.map +1 -0
  132. package/dist/store/paging.js +20 -0
  133. package/dist/store/search.d.ts +67 -0
  134. package/dist/store/search.d.ts.map +1 -0
  135. package/dist/store/search.js +160 -0
  136. package/dist/store/searchIndex.d.ts +53 -0
  137. package/dist/store/searchIndex.d.ts.map +1 -0
  138. package/dist/store/searchIndex.js +66 -0
  139. package/dist/store/threads.d.ts +147 -0
  140. package/dist/store/threads.d.ts.map +1 -0
  141. package/dist/store/threads.js +197 -0
  142. package/dist/submission/encoding.d.ts +21 -0
  143. package/dist/submission/encoding.d.ts.map +1 -0
  144. package/dist/submission/encoding.js +55 -0
  145. package/dist/submission/guard.d.ts +71 -0
  146. package/dist/submission/guard.d.ts.map +1 -0
  147. package/dist/submission/guard.js +46 -0
  148. package/dist/submission/submit.d.ts +152 -0
  149. package/dist/submission/submit.d.ts.map +1 -0
  150. package/dist/submission/submit.js +319 -0
  151. package/dist/version.generated.d.ts +5 -0
  152. package/dist/version.generated.d.ts.map +1 -0
  153. package/dist/version.generated.js +7 -0
  154. package/dist/workflows/classify.d.ts +63 -0
  155. package/dist/workflows/classify.d.ts.map +1 -0
  156. package/dist/workflows/classify.js +68 -0
  157. package/dist/workflows/retryPolicy.d.ts +39 -0
  158. package/dist/workflows/retryPolicy.d.ts.map +1 -0
  159. package/dist/workflows/retryPolicy.js +43 -0
  160. package/dist/workflows/specs.d.ts +43 -0
  161. package/dist/workflows/specs.d.ts.map +1 -0
  162. package/dist/workflows/specs.js +46 -0
  163. package/dist/workflows/worker.d.ts +48 -0
  164. package/dist/workflows/worker.d.ts.map +1 -0
  165. package/dist/workflows/worker.js +37 -0
  166. package/package.json +24 -15
  167. 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 };