@pithy-sh/support 0.1.2 → 0.1.4

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 (170) hide show
  1. package/dist/ai/classify.d.ts +92 -0
  2. package/dist/ai/classify.d.ts.map +1 -0
  3. package/dist/ai/classify.js +153 -0
  4. package/dist/attachment/store.d.ts +55 -0
  5. package/dist/attachment/store.d.ts.map +1 -0
  6. package/dist/attachment/store.js +69 -0
  7. package/dist/audit/actions.d.ts +70 -0
  8. package/dist/audit/actions.d.ts.map +1 -0
  9. package/dist/audit/actions.js +70 -0
  10. package/dist/capability.d.ts +95 -0
  11. package/dist/capability.d.ts.map +1 -0
  12. package/dist/capability.js +189 -0
  13. package/dist/client/projection.d.ts +58 -0
  14. package/dist/client/projection.d.ts.map +1 -0
  15. package/dist/client/projection.js +2 -0
  16. package/dist/config/config.d.ts +217 -0
  17. package/dist/config/config.d.ts.map +1 -0
  18. package/dist/config/config.js +171 -0
  19. package/dist/data/attachment.d.ts +32 -0
  20. package/dist/data/attachment.d.ts.map +1 -0
  21. package/dist/data/attachment.js +34 -0
  22. package/dist/data/billingScope.d.ts +32 -0
  23. package/dist/data/billingScope.d.ts.map +1 -0
  24. package/dist/data/billingScope.js +34 -0
  25. package/dist/data/categories.d.ts +56 -0
  26. package/dist/data/categories.d.ts.map +1 -0
  27. package/dist/data/categories.js +99 -0
  28. package/dist/data/classification.d.ts +36 -0
  29. package/dist/data/classification.d.ts.map +1 -0
  30. package/dist/data/classification.js +30 -0
  31. package/dist/data/enums.d.ts +84 -0
  32. package/dist/data/enums.d.ts.map +1 -0
  33. package/dist/data/enums.js +72 -0
  34. package/dist/data/flag.d.ts +27 -0
  35. package/dist/data/flag.d.ts.map +1 -0
  36. package/dist/data/flag.js +29 -0
  37. package/dist/data/message.d.ts +83 -0
  38. package/dist/data/message.d.ts.map +1 -0
  39. package/dist/data/message.js +86 -0
  40. package/dist/data/tables.d.ts +162 -0
  41. package/dist/data/tables.d.ts.map +1 -0
  42. package/dist/data/tables.js +46 -0
  43. package/dist/data/thread.d.ts +79 -0
  44. package/dist/data/thread.d.ts.map +1 -0
  45. package/dist/data/thread.js +67 -0
  46. package/dist/error/errors.d.ts +66 -0
  47. package/dist/error/errors.d.ts.map +1 -0
  48. package/dist/error/errors.js +84 -0
  49. package/dist/http/guards.d.ts +46 -0
  50. package/dist/http/guards.d.ts.map +1 -0
  51. package/dist/http/guards.js +55 -0
  52. package/dist/http/handlers.d.ts +91 -0
  53. package/dist/http/handlers.d.ts.map +1 -0
  54. package/dist/http/handlers.js +250 -0
  55. package/dist/http/resolve.d.ts +46 -0
  56. package/dist/http/resolve.d.ts.map +1 -0
  57. package/dist/http/resolve.js +74 -0
  58. package/dist/http/responses.d.ts +831 -0
  59. package/dist/http/responses.d.ts.map +1 -0
  60. package/dist/http/responses.js +305 -0
  61. package/dist/http/routes.d.ts +111 -0
  62. package/dist/http/routes.d.ts.map +1 -0
  63. package/dist/http/routes.js +64 -0
  64. package/dist/http/schemas.d.ts +123 -0
  65. package/dist/http/schemas.d.ts.map +1 -0
  66. package/dist/http/schemas.js +91 -0
  67. package/dist/http/scopes.d.ts +58 -0
  68. package/dist/http/scopes.d.ts.map +1 -0
  69. package/dist/http/scopes.js +109 -0
  70. package/dist/http/views.d.ts +38 -0
  71. package/dist/http/views.d.ts.map +1 -0
  72. package/dist/http/views.js +146 -0
  73. package/dist/inbound/authenticity.d.ts +83 -0
  74. package/dist/inbound/authenticity.d.ts.map +1 -0
  75. package/dist/inbound/authenticity.js +63 -0
  76. package/dist/inbound/guard.d.ts +76 -0
  77. package/dist/inbound/guard.d.ts.map +1 -0
  78. package/dist/inbound/guard.js +80 -0
  79. package/dist/inbound/handler.d.ts +30 -0
  80. package/dist/inbound/handler.d.ts.map +1 -0
  81. package/dist/inbound/handler.js +84 -0
  82. package/dist/inbound/ingest.d.ts +106 -0
  83. package/dist/inbound/ingest.d.ts.map +1 -0
  84. package/dist/inbound/ingest.js +328 -0
  85. package/dist/inbound/recipient.d.ts +18 -0
  86. package/dist/inbound/recipient.d.ts.map +1 -0
  87. package/dist/inbound/recipient.js +48 -0
  88. package/dist/index.d.ts +22 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +16 -0
  91. package/dist/link/sender.d.ts +162 -0
  92. package/dist/link/sender.d.ts.map +1 -0
  93. package/dist/link/sender.js +202 -0
  94. package/dist/migrations/0001_threads.d.ts +28 -0
  95. package/dist/migrations/0001_threads.d.ts.map +1 -0
  96. package/dist/migrations/0001_threads.js +105 -0
  97. package/dist/mime/address.d.ts +24 -0
  98. package/dist/mime/address.d.ts.map +1 -0
  99. package/dist/mime/address.js +30 -0
  100. package/dist/mime/parse.d.ts +98 -0
  101. package/dist/mime/parse.d.ts.map +1 -0
  102. package/dist/mime/parse.js +148 -0
  103. package/dist/mime/sanitize.d.ts +27 -0
  104. package/dist/mime/sanitize.d.ts.map +1 -0
  105. package/dist/mime/sanitize.js +214 -0
  106. package/dist/mime/threading.d.ts +81 -0
  107. package/dist/mime/threading.d.ts.map +1 -0
  108. package/dist/mime/threading.js +113 -0
  109. package/dist/mime/truncate.d.ts +19 -0
  110. package/dist/mime/truncate.d.ts.map +1 -0
  111. package/dist/mime/truncate.js +49 -0
  112. package/dist/provision/provisionSupport.d.ts +160 -0
  113. package/dist/provision/provisionSupport.d.ts.map +1 -0
  114. package/dist/provision/provisionSupport.js +112 -0
  115. package/dist/provision/resolveSupportConfig.d.ts +47 -0
  116. package/dist/provision/resolveSupportConfig.d.ts.map +1 -0
  117. package/dist/provision/resolveSupportConfig.js +24 -0
  118. package/dist/reply/send.d.ts +125 -0
  119. package/dist/reply/send.d.ts.map +1 -0
  120. package/dist/reply/send.js +155 -0
  121. package/dist/reply/snippets.d.ts +81 -0
  122. package/dist/reply/snippets.d.ts.map +1 -0
  123. package/dist/reply/snippets.js +137 -0
  124. package/dist/secret/registry.d.ts +20 -0
  125. package/dist/secret/registry.d.ts.map +1 -0
  126. package/dist/secret/registry.js +23 -0
  127. package/dist/seeds/example.d.ts +5 -0
  128. package/dist/seeds/example.d.ts.map +1 -0
  129. package/dist/seeds/example.js +352 -0
  130. package/dist/store/paging.d.ts +20 -0
  131. package/dist/store/paging.d.ts.map +1 -0
  132. package/dist/store/paging.js +22 -0
  133. package/dist/store/search.d.ts +69 -0
  134. package/dist/store/search.d.ts.map +1 -0
  135. package/dist/store/search.js +162 -0
  136. package/dist/store/searchIndex.d.ts +55 -0
  137. package/dist/store/searchIndex.d.ts.map +1 -0
  138. package/dist/store/searchIndex.js +68 -0
  139. package/dist/store/threads.d.ts +149 -0
  140. package/dist/store/threads.d.ts.map +1 -0
  141. package/dist/store/threads.js +199 -0
  142. package/dist/submission/encoding.d.ts +23 -0
  143. package/dist/submission/encoding.d.ts.map +1 -0
  144. package/dist/submission/encoding.js +57 -0
  145. package/dist/submission/guard.d.ts +73 -0
  146. package/dist/submission/guard.d.ts.map +1 -0
  147. package/dist/submission/guard.js +48 -0
  148. package/dist/submission/submit.d.ts +154 -0
  149. package/dist/submission/submit.d.ts.map +1 -0
  150. package/dist/submission/submit.js +321 -0
  151. package/dist/version.generated.d.ts +7 -0
  152. package/dist/version.generated.d.ts.map +1 -0
  153. package/dist/version.generated.js +9 -0
  154. package/dist/workflows/classify.d.ts +65 -0
  155. package/dist/workflows/classify.d.ts.map +1 -0
  156. package/dist/workflows/classify.js +70 -0
  157. package/dist/workflows/retryPolicy.d.ts +41 -0
  158. package/dist/workflows/retryPolicy.d.ts.map +1 -0
  159. package/dist/workflows/retryPolicy.js +45 -0
  160. package/dist/workflows/specs.d.ts +45 -0
  161. package/dist/workflows/specs.d.ts.map +1 -0
  162. package/dist/workflows/specs.js +48 -0
  163. package/dist/workflows/worker.d.ts +50 -0
  164. package/dist/workflows/worker.d.ts.map +1 -0
  165. package/dist/workflows/worker.js +39 -0
  166. package/package.json +24 -15
  167. package/src/http/handlers.ts +1 -1
  168. package/src/submission/encoding.ts +1 -1
  169. package/src/submission/submit.ts +1 -1
  170. package/src/version.generated.ts +1 -1
@@ -0,0 +1,328 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { SupportAttachment } from "../data/attachment.js";
4
+ import { SupportMessage } from "../data/message.js";
5
+ import { SupportThread, UNCLASSIFIED } from "../data/thread.js";
6
+ import { SUPPORT_ATTACHMENTS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_THREADS_TABLE } from "../data/tables.js";
7
+ import { attachmentKey, putAttachment, rawMessageKey, sha256Hex } from "../attachment/store.js";
8
+ import { SupportAuditActions } from "../audit/actions.js";
9
+ import { parentCandidates } from "../mime/threading.js";
10
+ import { indexMessage } from "../store/search.js";
11
+ import { truncateToBytes } from "../mime/truncate.js";
12
+ import { MAX_TEXT_BODY, parseInbound } from "../mime/parse.js";
13
+ import { checkRates, checkSize } from "./guard.js";
14
+ import { htmlToText, sanitizeHtml } from "../mime/sanitize.js";
15
+ import { senderAuthenticity } from "./authenticity.js";
16
+ import { resolveInbox } from "./recipient.js";
17
+ import { chunkRowsByBoundParameters } from "@pithy-sh/core/src/data/boundParameters";
18
+ //#region src/inbound/ingest.ts
19
+ /** Record a refusal. Never throws — a guard decision must not become a 500. */
20
+ async function auditRejection(deps, reason, detail, fromAddress) {
21
+ try {
22
+ await deps.emit({
23
+ action: SupportAuditActions.inboundRejected,
24
+ outcome: "denied",
25
+ severity: "warning",
26
+ actorType: "anonymous",
27
+ actorId: null,
28
+ resourceType: "support_inbox",
29
+ metadata: {
30
+ reason,
31
+ fromAddress: fromAddress ?? null
32
+ }
33
+ });
34
+ deps.log.warn("support inbound message refused", {
35
+ reason,
36
+ detail,
37
+ fromAddress: fromAddress ?? null
38
+ });
39
+ } catch (error) {
40
+ deps.log.warn("support inbound rejection audit dropped", {
41
+ reason,
42
+ detail,
43
+ error
44
+ });
45
+ }
46
+ }
47
+ /**
48
+ * Find the thread a message continues, by walking its threading chain most-precise-first.
49
+ *
50
+ * The lookup goes through `pithy_support_messages.mime_message_id`, which is uniquely indexed — so
51
+ * this is an index seek per candidate and the chain is bounded at 50 entries. It stops at the first
52
+ * hit, which is why `parentCandidates` puts `In-Reply-To` ahead of a reversed `References`.
53
+ */
54
+ async function findParentThread(db, parsed, inbox) {
55
+ const candidates = parentCandidates(parsed.inReplyTo, parsed.references);
56
+ if (candidates.length === 0) return void 0;
57
+ const parent = await db.selectFrom(SUPPORT_MESSAGES_TABLE).innerJoin(SUPPORT_THREADS_TABLE, `${SUPPORT_THREADS_TABLE}.id`, `${SUPPORT_MESSAGES_TABLE}.threadId`).select([`${SUPPORT_MESSAGES_TABLE}.threadId`, `${SUPPORT_MESSAGES_TABLE}.mimeMessageId`]).where(`${SUPPORT_MESSAGES_TABLE}.mimeMessageId`, "in", candidates).where(`${SUPPORT_THREADS_TABLE}.inboxAddress`, "=", inbox).execute();
58
+ if (parent.length === 0) return void 0;
59
+ const byId = new Map(parent.map((row) => [row.mimeMessageId, row.threadId]));
60
+ for (const candidate of candidates) {
61
+ const threadId = byId.get(candidate);
62
+ if (threadId !== void 0) return threadId;
63
+ }
64
+ }
65
+ /**
66
+ * How many columns one `pithy_support_attachments` row binds. Derived from the schema rather than
67
+ * written as a literal, so adding a column re-chunks instead of silently re-breaking the cap.
68
+ */
69
+ const SUPPORT_ATTACHMENT_COLUMNS = Object.keys(SupportAttachment.shape).length;
70
+ /** Store the attachments this config allows, and return the rows written. */
71
+ async function storeAttachments(deps, parsed, threadId, messageId, now) {
72
+ const { attachments: bounds } = deps.config;
73
+ const bucket = deps.bucket;
74
+ if (!bounds.enabled || !bucket || parsed.attachments.length === 0) return 0;
75
+ const accepted = parsed.attachments.slice(0, bounds.maxCount);
76
+ if (parsed.attachments.length > accepted.length) deps.log.warn("support attachments truncated", {
77
+ threadId,
78
+ declared: parsed.attachments.length,
79
+ stored: accepted.length
80
+ });
81
+ const rows = [];
82
+ for (const attachment of accepted) {
83
+ if (attachment.bytes.byteLength > bounds.maxBytes) {
84
+ deps.log.warn("support attachment skipped, over the size bound", {
85
+ threadId,
86
+ bytes: attachment.bytes.byteLength,
87
+ maxBytes: bounds.maxBytes
88
+ });
89
+ continue;
90
+ }
91
+ const id = deps.newId();
92
+ const key = attachmentKey(threadId, id);
93
+ await putAttachment(bucket, key, attachment.bytes);
94
+ rows.push({
95
+ id,
96
+ messageId,
97
+ threadId,
98
+ filename: attachment.filename,
99
+ contentType: attachment.contentType,
100
+ size: attachment.bytes.byteLength,
101
+ sha256: await sha256Hex(attachment.bytes),
102
+ storageKey: key,
103
+ contentId: attachment.contentId ?? null,
104
+ inline: attachment.inline,
105
+ createdAt: now
106
+ });
107
+ }
108
+ const encoded = rows.map((row) => SupportAttachment.encode(row));
109
+ for (const chunk of chunkRowsByBoundParameters(encoded, SUPPORT_ATTACHMENT_COLUMNS)) await deps.db.insertInto(SUPPORT_ATTACHMENTS_TABLE).values(chunk).execute();
110
+ return rows.length;
111
+ }
112
+ /** Ingest one inbound message. */
113
+ async function ingestInbound(deps, input) {
114
+ const now = deps.now();
115
+ const rawBytes = input.raw.byteLength;
116
+ const size = checkSize(deps.config.guard, rawBytes);
117
+ if (!size.accepted) {
118
+ await auditRejection(deps, size.reason, size.detail, void 0);
119
+ return {
120
+ handled: false,
121
+ reason: "rejected",
122
+ rejection: size.reason
123
+ };
124
+ }
125
+ const parsed = await parseInbound(input.raw, { expectedAuthservId: deps.config.guard.authservId });
126
+ const inbox = resolveInbox({
127
+ inboundAddresses: deps.config.inboundAddresses,
128
+ envelopeTo: input.envelopeTo,
129
+ headerRecipients: parsed.headerRecipients
130
+ });
131
+ if (inbox === void 0) return {
132
+ handled: false,
133
+ reason: "not_addressed"
134
+ };
135
+ const rates = await checkRates(deps.db, deps.config.guard, {
136
+ rawBytes,
137
+ fromAddress: parsed.fromAddress,
138
+ now
139
+ });
140
+ if (!rates.accepted) {
141
+ await auditRejection(deps, rates.reason, rates.detail, parsed.fromAddress);
142
+ return {
143
+ handled: false,
144
+ reason: "rejected",
145
+ rejection: rates.reason
146
+ };
147
+ }
148
+ if (parsed.messageId !== void 0) {
149
+ const existing = await deps.db.selectFrom(SUPPORT_MESSAGES_TABLE).select(["id", "threadId"]).where("mimeMessageId", "=", parsed.messageId).where("toAddress", "=", inbox).executeTakeFirst();
150
+ if (existing) {
151
+ deps.log.info("support message already stored, ignoring redelivery", { messageId: existing.id });
152
+ return {
153
+ handled: true,
154
+ duplicate: true,
155
+ threadId: existing.threadId,
156
+ messageId: existing.id
157
+ };
158
+ }
159
+ }
160
+ const html = parsed.html !== void 0 ? await sanitizeHtml(parsed.html) : void 0;
161
+ const derived = parsed.text.trim().length > 0 ? parsed.text : parsed.html !== void 0 ? await htmlToText(parsed.html) : "";
162
+ const text = truncateToBytes(derived, MAX_TEXT_BODY);
163
+ const authenticity = senderAuthenticity({
164
+ authResults: parsed.authResults,
165
+ fromAddress: parsed.fromAddress,
166
+ envelopeFrom: input.envelopeFrom,
167
+ trusted: deps.config.guard.trustAuthenticationResults
168
+ });
169
+ deps.log.info("support inbound authentication headers observed", {
170
+ seen: parsed.authHeadersSeen,
171
+ trusted: deps.config.guard.trustAuthenticationResults,
172
+ authenticated: authenticity.authenticated
173
+ });
174
+ const parentThreadId = await findParentThread(deps.db, parsed, inbox);
175
+ const threadId = parentThreadId ?? deps.newId();
176
+ const newThread = parentThreadId === void 0;
177
+ const messageId = deps.newId();
178
+ const subject = parsed.subject.length > 0 ? parsed.subject : "(no subject)";
179
+ let rawKey = null;
180
+ if (deps.bucket && deps.config.attachments.retainRaw) {
181
+ rawKey = rawMessageKey(threadId, messageId);
182
+ await deps.bucket.put(rawKey, input.raw, { httpMetadata: {
183
+ contentType: "application/octet-stream",
184
+ contentDisposition: "attachment"
185
+ } });
186
+ }
187
+ const message = {
188
+ id: messageId,
189
+ threadId,
190
+ direction: "inbound",
191
+ channel: "email",
192
+ submittedByUserId: null,
193
+ context: null,
194
+ mimeMessageId: parsed.messageId ?? null,
195
+ mimeInReplyTo: parsed.inReplyTo ?? null,
196
+ mimeReferences: parsed.references.length > 0 ? parsed.references : null,
197
+ fromAddress: parsed.fromAddress,
198
+ fromName: parsed.fromName ?? null,
199
+ toAddress: inbox,
200
+ subject,
201
+ textBody: text,
202
+ htmlBody: html ?? null,
203
+ emailJobId: null,
204
+ rawKey,
205
+ rawBytes,
206
+ receivedAt: now,
207
+ createdAt: now
208
+ };
209
+ try {
210
+ await deps.db.insertInto(SUPPORT_MESSAGES_TABLE).values(SupportMessage.encode(message)).execute();
211
+ } catch (error) {
212
+ const existing = parsed.messageId ? await deps.db.selectFrom(SUPPORT_MESSAGES_TABLE).select(["id", "threadId"]).where("mimeMessageId", "=", parsed.messageId).where("toAddress", "=", inbox).executeTakeFirst() : void 0;
213
+ if (existing) {
214
+ deps.log.info("support message stored concurrently, ignoring redelivery", { messageId: existing.id });
215
+ return {
216
+ handled: true,
217
+ duplicate: true,
218
+ threadId: existing.threadId,
219
+ messageId: existing.id
220
+ };
221
+ }
222
+ throw error;
223
+ }
224
+ try {
225
+ if (newThread) {
226
+ let userId = null;
227
+ try {
228
+ userId = await deps.linkSender(parsed.fromAddress);
229
+ } catch (error) {
230
+ deps.log.warn("support sender link failed", { error });
231
+ }
232
+ if (userId && !authenticity.authenticated) deps.log.info("support sender matched on an unverified From address", { method: authenticity.method });
233
+ const thread = {
234
+ id: threadId,
235
+ channel: "email",
236
+ inboxAddress: inbox,
237
+ subject,
238
+ fromAddress: parsed.fromAddress,
239
+ fromName: parsed.fromName ?? null,
240
+ senderAuthenticated: authenticity.authenticated,
241
+ userId,
242
+ accountLinkSource: userId ? "email_address" : null,
243
+ ...UNCLASSIFIED,
244
+ archived: false,
245
+ archivedAt: null,
246
+ archivedBy: null,
247
+ messageCount: 1,
248
+ firstMessageAt: now,
249
+ lastMessageAt: now,
250
+ createdAt: now,
251
+ updatedAt: now
252
+ };
253
+ await deps.db.insertInto(SUPPORT_THREADS_TABLE).values(SupportThread.encode(thread)).execute();
254
+ } else {
255
+ let linkedNow = null;
256
+ if (authenticity.authenticated) {
257
+ if ((await deps.db.selectFrom("pithySupportThreads").select(["userId"]).where("id", "=", threadId).executeTakeFirst())?.userId == null) try {
258
+ linkedNow = await deps.linkSender(parsed.fromAddress);
259
+ } catch (error) {
260
+ deps.log.warn("support sender re-link failed", { error });
261
+ }
262
+ }
263
+ await deps.db.updateTable(SUPPORT_THREADS_TABLE).set((eb) => ({
264
+ ...linkedNow ? {
265
+ userId: linkedNow,
266
+ senderAuthenticated: 1,
267
+ accountLinkSource: "email_address"
268
+ } : {},
269
+ messageCount: eb("messageCount", "+", 1),
270
+ lastMessageAt: now.getTime(),
271
+ updatedAt: now.getTime(),
272
+ archived: 0,
273
+ archivedAt: null,
274
+ archivedBy: null
275
+ })).where("id", "=", threadId).execute();
276
+ }
277
+ } catch (error) {
278
+ await deps.db.deleteFrom(SUPPORT_MESSAGES_TABLE).where("id", "=", messageId).execute().catch(() => {
279
+ deps.log.error("support message orphaned — thread write failed and the message could not be removed", {
280
+ messageId,
281
+ threadId
282
+ });
283
+ });
284
+ throw error;
285
+ }
286
+ if (deps.fts) try {
287
+ await indexMessage(deps.db, {
288
+ threadId,
289
+ messageId,
290
+ subject,
291
+ body: text
292
+ });
293
+ } catch (error) {
294
+ deps.log.warn("support message not indexed", {
295
+ messageId,
296
+ error
297
+ });
298
+ }
299
+ let attachments = 0;
300
+ try {
301
+ attachments = await storeAttachments(deps, parsed, threadId, messageId, now);
302
+ } catch (error) {
303
+ deps.log.warn("support attachments not stored", {
304
+ messageId,
305
+ error
306
+ });
307
+ }
308
+ const classifying = deps.config.ai.enabled && !parsed.autoSubmitted;
309
+ if (classifying) try {
310
+ await deps.dispatchClassify(messageId);
311
+ } catch (error) {
312
+ deps.log.warn("support classification dispatch failed", {
313
+ messageId,
314
+ error
315
+ });
316
+ }
317
+ return {
318
+ handled: true,
319
+ duplicate: false,
320
+ threadId,
321
+ messageId,
322
+ newThread,
323
+ attachments,
324
+ classifying
325
+ };
326
+ }
327
+ //#endregion
328
+ export { ingestInbound };
@@ -0,0 +1,18 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Resolve which configured inbox a message landed on, or `undefined` when none of them did.
5
+ *
6
+ * The returned address is stored on the thread, so a Worker serving `support@` and `security@` can
7
+ * keep the two apart in one table — and the value is the *configured* one rather than the envelope
8
+ * string, so a thread's inbox is always something that appears in `pithy.config.ts`.
9
+ */
10
+ export declare function resolveInbox(options: {
11
+ /** The configured addresses, already normalized and lowercased. */
12
+ inboundAddresses: readonly string[];
13
+ /** The SMTP envelope recipient — `ForwardableEmailMessage.to`. The authority. */
14
+ envelopeTo: string | undefined;
15
+ /** `To`/`Cc`/`Delivered-To` from the parsed headers. Corroborating only. */
16
+ headerRecipients: readonly string[];
17
+ }): string | undefined;
18
+ //# sourceMappingURL=recipient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipient.d.ts","sourceRoot":"","sources":["../../src/inbound/recipient.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,mEAAmE;IACnE,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iFAAiF;IACjF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CAoBrB"}
@@ -0,0 +1,48 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { parseAddress } from "@pithy-sh/core/src/address/address";
4
+ //#region src/inbound/recipient.ts
5
+ /**
6
+ * Which inbound messages belong to this capability.
7
+ *
8
+ * A Worker has exactly one `email()` entry, and `createEntrypoint` fans every message to **every**
9
+ * capability that declares a handler. So a Worker running both `@pithy-sh/email` and this one sees
10
+ * each bounce twice and each support request twice, and the discrimination is each handler's own
11
+ * job. Email's handler filters by content — it acts only on DSNs and ARF reports. This one filters
12
+ * by **address**, because a support request looks like ordinary mail and there is nothing in its
13
+ * body to key on.
14
+ *
15
+ * ## The envelope recipient is the authority; the headers are not
16
+ *
17
+ * `ForwardableEmailMessage.to` is the SMTP envelope recipient — the address Cloudflare's own routing
18
+ * rule matched to deliver here. It is the only recipient that was proved rather than asserted.
19
+ *
20
+ * The `To:` and `Cc:` headers are neither. Anyone can put `To: support@yourdomain.com` on a message
21
+ * addressed elsewhere, and BCC means a legitimate message frequently does not name its real
22
+ * recipient at all. Trusting them would let a stranger inject threads into an inbox they were never
23
+ * routed to — so they are used only as a fallback for the one case where the envelope is unusable,
24
+ * and never to override it.
25
+ */
26
+ /** Whether an address is one of the configured inbox addresses. Both sides are already normalized. */
27
+ function claims(addresses, candidate) {
28
+ return candidate !== void 0 && addresses.includes(candidate);
29
+ }
30
+ /**
31
+ * Resolve which configured inbox a message landed on, or `undefined` when none of them did.
32
+ *
33
+ * The returned address is stored on the thread, so a Worker serving `support@` and `security@` can
34
+ * keep the two apart in one table — and the value is the *configured* one rather than the envelope
35
+ * string, so a thread's inbox is always something that appears in `pithy.config.ts`.
36
+ */
37
+ function resolveInbox(options) {
38
+ const { inboundAddresses } = options;
39
+ if (inboundAddresses.length === 0) return void 0;
40
+ const envelope = parseAddress(options.envelopeTo);
41
+ if (envelope !== void 0) return claims(inboundAddresses, envelope) ? envelope : void 0;
42
+ for (const candidate of options.headerRecipients) {
43
+ const normalized = parseAddress(candidate);
44
+ if (claims(inboundAddresses, normalized)) return normalized;
45
+ }
46
+ }
47
+ //#endregion
48
+ export { resolveInbox };
@@ -0,0 +1,22 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * The package entrypoint — the surface `pithy add support` wires into `pithy.config.ts`. Deliberately
5
+ * narrow: the capability factory, its config and options types, the two federation helpers an adopter
6
+ * calls, the control-plane scopes, and the table schemas. Every other module is imported by deep path
7
+ * (`@pithy-sh/support/src/...`); this is the documented contract, not a barrel over the package.
8
+ */
9
+ export { type SupportAuditAction, SupportAuditActions } from "./audit/actions";
10
+ export { isSupportCapability, SUPPORT_MIGRATION_ORDER, type SupportCapability, type SupportOptions, support, } from "./capability";
11
+ export { DEFAULT_CLASSIFY_MODEL, SupportAiConfig, SupportAttachmentsConfig, SupportConfig, type SupportConfigInput, SupportGuardConfig, SupportReplyConfig, SupportSearchConfig, } from "./config/config";
12
+ export { SupportAttachment } from "./data/attachment";
13
+ export { DEFAULT_SUPPORT_CATEGORIES, defineSupportCategories, type SupportCategories, } from "./data/categories";
14
+ export { SupportClassification } from "./data/classification";
15
+ export { SupportMessageDirection, SupportPriority, SupportSentiment, UNCATEGORIZED } from "./data/enums";
16
+ export { SupportThreadFlag } from "./data/flag";
17
+ export { SupportMessage } from "./data/message";
18
+ export { SUPPORT_ATTACHMENTS_TABLE, SUPPORT_CLASSIFICATIONS_TABLE, SUPPORT_FLAGS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_SEARCH_TABLE, SUPPORT_THREADS_TABLE, type SupportDatabase, supportDatabase, } from "./data/tables";
19
+ export { SupportThread } from "./data/thread";
20
+ export { SUPPORT_CONTROL_PLANE_SCOPES, SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE, } from "./http/scopes";
21
+ export { DEFAULT_SUPPORT_REPLIES, defineSupportReplies, SupportReplySnippet, type SupportReplySnippets, } from "./reply/snippets";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,OAAO,GACR,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,sBAAsB,EACtB,eAAe,EACf,wBAAwB,EACxB,aAAa,EACb,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EACL,0BAA0B,EAC1B,uBAAuB,EACvB,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,eAAe,EACpB,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,EAC7B,0BAA0B,EAC1B,0BAA0B,EAC1B,gCAAgC,EAChC,2BAA2B,GAC5B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { DEFAULT_SUPPORT_REPLIES, SupportReplySnippet, defineSupportReplies } from "./reply/snippets.js";
4
+ import { DEFAULT_CLASSIFY_MODEL, SupportAiConfig, SupportAttachmentsConfig, SupportConfig, SupportGuardConfig, SupportReplyConfig, SupportSearchConfig } from "./config/config.js";
5
+ import { SupportMessageDirection, SupportPriority, SupportSentiment, UNCATEGORIZED } from "./data/enums.js";
6
+ import { DEFAULT_SUPPORT_CATEGORIES, defineSupportCategories } from "./data/categories.js";
7
+ import { SupportAttachment } from "./data/attachment.js";
8
+ import { SupportClassification } from "./data/classification.js";
9
+ import { SupportThreadFlag } from "./data/flag.js";
10
+ import { SupportMessage } from "./data/message.js";
11
+ import { SupportThread } from "./data/thread.js";
12
+ import { SUPPORT_ATTACHMENTS_TABLE, SUPPORT_CLASSIFICATIONS_TABLE, SUPPORT_FLAGS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_SEARCH_TABLE, SUPPORT_THREADS_TABLE, supportDatabase } from "./data/tables.js";
13
+ import { SupportAuditActions } from "./audit/actions.js";
14
+ import { SUPPORT_CONTROL_PLANE_SCOPES, SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE } from "./http/scopes.js";
15
+ import { SUPPORT_MIGRATION_ORDER, isSupportCapability, support } from "./capability.js";
16
+ export { DEFAULT_CLASSIFY_MODEL, DEFAULT_SUPPORT_CATEGORIES, DEFAULT_SUPPORT_REPLIES, SUPPORT_ATTACHMENTS_TABLE, SUPPORT_CLASSIFICATIONS_TABLE, SUPPORT_CONTROL_PLANE_SCOPES, SUPPORT_FLAGS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_MIGRATION_ORDER, SUPPORT_SEARCH_TABLE, SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE, SUPPORT_THREADS_TABLE, SupportAiConfig, SupportAttachment, SupportAttachmentsConfig, SupportAuditActions, SupportClassification, SupportConfig, SupportGuardConfig, SupportMessage, SupportMessageDirection, SupportPriority, SupportReplyConfig, SupportReplySnippet, SupportSearchConfig, SupportSentiment, SupportThread, SupportThreadFlag, UNCATEGORIZED, defineSupportCategories, defineSupportReplies, isSupportCapability, support, supportDatabase };
@@ -0,0 +1,162 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { D1Database } from "@cloudflare/workers-types";
4
+ import type { Entitlement } from "@pithy-sh/core/src/entitlement/entitlement";
5
+ /**
6
+ * The link from an address in a `From` header to the customer the app already knows about.
7
+ *
8
+ * This is the feature the whole capability exists for. A support thread that arrives already knowing
9
+ * who the sender is, what they bought, on which rail, and whether their last renewal failed is a
10
+ * different object from a message in a mailbox — and every one of those facts is already in the
11
+ * adopter's own D1, one table over, unused.
12
+ *
13
+ * ## Guarded dynamic imports, not dependencies
14
+ *
15
+ * `@pithy-sh/auth` and `@pithy-sh/payments` are **optional** here (principle 4: depend on core seams,
16
+ * never on a sibling's internals), so both are reached the way `@pithy-sh/matchmaking` already
17
+ * reaches auth — a dynamic import inside a `try`, degrading to "no link" when the package is not
18
+ * installed. A support inbox in a project with no accounts and no payments is a perfectly reasonable
19
+ * thing to run, and it must not fail to start, or to store mail, because of what it cannot see.
20
+ *
21
+ * Every function here is **best-effort by contract**: it returns nothing rather than throwing, and
22
+ * the caller stores the message either way. Linkage is context, and context is never worth losing a
23
+ * customer's support request over.
24
+ *
25
+ * ## The billing half links people, and only people
26
+ *
27
+ * `@pithy-sh/payments` keys a purchase on a **subject pair** — a user or an organization, plus an id —
28
+ * and which of the two a project uses is its `billingSubject` config. Support cannot honor that choice.
29
+ * It starts from a `From:` header, resolves it to a *person*, and does its billing lookup at thread-read
30
+ * time, where there is no Hono `Context` to hand the adopter's subject resolver: the seam's whole job is
31
+ * to answer "which organization is *this caller* acting for", and a support thread has no caller.
32
+ *
33
+ * So this file reads `user`-subject rows, and {@link SUPPORT_BILLING_SCOPE} says so on every response.
34
+ * Under organization billing the panel is empty, and it must not be *silently* empty — the lookups below
35
+ * are guarded dynamic imports whose `catch` already returns `[]`, so an empty panel is indistinguishable
36
+ * between "bought nothing", "`@pithy-sh/payments` is not installed" and "billed to an organization".
37
+ * An operator reads the first of those and decides a refund on it. A declared scope is what turns the
38
+ * third case from a silence into a limitation somebody can see.
39
+ *
40
+ * **The scope constant used to be declared here, and is now `data/billingScope.ts` (Jim, 2026-08-21).**
41
+ * The old argument was that the query and the wire must read one value, which is right and unchanged —
42
+ * but `http/responses.ts` honoring it by importing from *this* file pulled the whole server data layer
43
+ * into a browser program, because that is what this file's imports are (#419). One value, in a module
44
+ * neither half owns.
45
+ */
46
+ /** The account a sender resolves to, and what the app knows about them. */
47
+ export interface SenderContext {
48
+ /**
49
+ * Whether the `From:` header was proved to belong to the sender. **Everything below is empty when
50
+ * this is false**, and a dashboard must render the sender as an unverified claim rather than as a
51
+ * customer — the whole value of this panel is that an operator trusts it, so it must not be
52
+ * populated from an address anybody could have written.
53
+ */
54
+ authenticated: boolean;
55
+ /** The linked `pithy_auth_users.id`, or null when this address belongs to nobody with an account. */
56
+ userId: string | null;
57
+ /** The account's display name, when auth is composed and the sender is known. */
58
+ name?: string;
59
+ /** Whether the account has verified this address. A useful signal beside an unverified claim in a header. */
60
+ emailVerified?: boolean;
61
+ /**
62
+ * Their purchase history, newest first — **individually billed purchases only**. Empty when payments
63
+ * is absent, when they have bought nothing, and when their billing is held by an organization this
64
+ * seam cannot resolve. `SenderContextView.billingScope` is what tells a console those apart.
65
+ */
66
+ purchases: readonly SenderPurchase[];
67
+ /**
68
+ * Their entitlements, lapsed ones included and marked inactive — a paywall wants to say when Pro
69
+ * ended. Same scope as {@link SenderContext.purchases}: `user` subjects, nothing organization-held.
70
+ */
71
+ entitlements: readonly Entitlement[];
72
+ }
73
+ /** One purchase, flattened to what a support console renders. */
74
+ export interface SenderPurchase {
75
+ /** The purchase row id. */
76
+ id: string;
77
+ /** Which rail it went through — `apple`, `google`, or `stripe`. */
78
+ rail: string;
79
+ /** The Pithy product key. */
80
+ productId: string;
81
+ /** Its lifecycle state — `active`, `refunded`, `expired`, and so on. */
82
+ status: string;
83
+ /** Whether the purchase happened in the store's sandbox rather than production. */
84
+ environment: string;
85
+ /** When it was bought. */
86
+ purchasedAt: Date;
87
+ /** When it runs out; null for something owned forever. */
88
+ expiresAt: Date | null;
89
+ /** When it was refunded or revoked; null while it stands. */
90
+ revokedAt: Date | null;
91
+ }
92
+ /** How many purchases a thread view carries. Enough to see the pattern, bounded so a whale is not a slow page. */
93
+ export declare const MAX_LINKED_PURCHASES = 25;
94
+ /**
95
+ * Resolve a sender address to a user id.
96
+ *
97
+ * **Exact match on the normalized address, and deliberately no case-insensitive fallback.** The
98
+ * `email` column is indexed and unique, so this is one index seek; a `lower(email) = ?` fallback
99
+ * would be a full table scan, and it would run for *every unknown sender* — which on a public
100
+ * address means every piece of spam scanning the entire user table. Better Auth normalizes on its
101
+ * own signup path, so the exact match is the case that actually occurs; an address stored with
102
+ * capitals by some other route simply does not link, which costs a line of context rather than a
103
+ * customer's request.
104
+ */
105
+ export declare function resolveSenderUserId(d1: D1Database, address: string): Promise<string | null>;
106
+ /**
107
+ * The address an operator's reply to an app thread will be sent to, or null when the account carries
108
+ * nothing deliverable.
109
+ *
110
+ * Extracted rather than inlined because it is the one decision in {@link resolveSubmitterAccount} with
111
+ * a wrong answer available, and inlining it puts that decision behind a guarded dynamic import where
112
+ * no test can reach it.
113
+ *
114
+ * **`parseAddress` only, with no `normalizeAddress` fallback.** The fallback merely trims and
115
+ * lowercases, so it would hand back an address this capability's own parser had just refused — and
116
+ * this value becomes the thread's `fromAddress`, which `sendReply` enqueues an answer to. Returning
117
+ * null makes an unparseable account the same hard fault as a missing one, which is what the caller
118
+ * already does with it: refusing the submission beats accepting a report whose only reply address
119
+ * cannot be delivered to.
120
+ */
121
+ export declare function submitterAddress(email: string | null | undefined): string | null;
122
+ /**
123
+ * The account behind an authenticated submitter — resolved by id, never by an address.
124
+ *
125
+ * The inverse of {@link resolveSenderUserId}, and it exists because the in-app channel starts from the
126
+ * opposite end: a session names a user id, and what the thread needs is the address a reply will go
127
+ * back to. Deriving that from anything the client sent would hand a signed-in caller the ability to
128
+ * point a support conversation — and every operator reply on it — at somebody else's mailbox.
129
+ *
130
+ * Returns null when `@pithy-sh/auth` is absent or the account is gone. A submission whose account
131
+ * cannot be read is refused by the caller rather than stored with a guessed address: an app thread with
132
+ * no working reply address is a report nobody can answer.
133
+ */
134
+ export declare function resolveSubmitterAccount(d1: D1Database, userId: string): Promise<{
135
+ email: string;
136
+ name?: string;
137
+ emailVerified?: boolean;
138
+ } | null>;
139
+ /**
140
+ * Everything the app already knows about a sender — read at thread-read time rather than stored.
141
+ *
142
+ * Derived, so it is always current: a customer who buys Pro an hour after writing in shows as
143
+ * entitled the next time the thread is opened, with nothing to backfill and nothing to invalidate.
144
+ * That is the same rule the classification follows, applied to the half of the model that lives in
145
+ * somebody else's tables.
146
+ */
147
+ export declare function resolveSenderContext(d1: D1Database, address: string, now: Date, options: {
148
+ authenticated: boolean;
149
+ }): Promise<SenderContext>;
150
+ /**
151
+ * The customer context behind a **session-proven** link, resolved from the user id itself.
152
+ *
153
+ * An app thread already holds the id its session proved, so re-deriving it from the thread's address
154
+ * would be both a step backwards and a correctness bug: `resolveSenderUserId` matches the `email`
155
+ * column exactly, and an account stored with capitals by some other route would silently resolve to
156
+ * nobody — turning the one link that *is* certain into the one the console shows as unknown.
157
+ *
158
+ * Everything below the link degrades exactly as it does on the mail path: purchases and entitlements
159
+ * are guarded dynamic imports and come back empty when `@pithy-sh/payments` is absent.
160
+ */
161
+ export declare function resolveSubmitterContext(d1: D1Database, userId: string, now: Date): Promise<SenderContext>;
162
+ //# sourceMappingURL=sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.d.ts","sourceRoot":"","sources":["../../src/link/sender.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAG9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,qGAAqG;IACrG,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6GAA6G;IAC7G,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IACrC;;;OAGG;IACH,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;CACtC;AAED,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,WAAW,EAAE,IAAI,CAAC;IAClB,0DAA0D;IAC1D,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,6DAA6D;IAC7D,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,kHAAkH;AAClH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBjG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEhF;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAiB3E;AA4ED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,IAAI,EACT,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE,GAClC,OAAO,CAAC,aAAa,CAAC,CAwBxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,CAI/G"}