@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,79 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ /**
5
+ * One conversation in `pithy_support_threads` — the row the inbox is a list of.
6
+ *
7
+ * **Everything on it is derived from immutable inbound mail.** The classification is recomputed, never
8
+ * repaired; the user link is a lookup, not a decision; the counters are facts about the messages
9
+ * below. There is no assignee and no status, because a status field is a state machine and a state
10
+ * machine attracts SLAs, escalation, and reporting — which is a ticketing product and explicitly not
11
+ * this. The two exceptions are deliberate and named: `archived` (one shared boolean meaning done) and
12
+ * the per-viewer flags, which live in their own table because nobody coordinates around them.
13
+ *
14
+ * The classification axes are **denormalized onto the thread** rather than joined from
15
+ * `pithy_support_classifications`. The whole dashboard interaction is "newest first, filtered by
16
+ * category and priority and archived", and that is a composite-index workload — a join to find the
17
+ * latest classification per thread would make the one query this table exists to serve the expensive
18
+ * one. The classification table keeps the history; this keeps the answer.
19
+ */
20
+ export declare const SupportThread: z.ZodObject<{
21
+ id: z.ZodString;
22
+ channel: z.ZodEnum<{
23
+ app: "app";
24
+ email: "email";
25
+ }>;
26
+ inboxAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ subject: z.ZodString;
28
+ fromAddress: z.ZodString;
29
+ fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ senderAuthenticated: z.ZodCodec<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodBoolean, z.ZodString]>, z.ZodBoolean>;
31
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
32
+ accountLinkSource: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
33
+ email_address: "email_address";
34
+ session: "session";
35
+ }>>>;
36
+ declaredCategory: z.ZodOptional<z.ZodNullable<z.ZodString>>;
37
+ category: z.ZodString;
38
+ priority: z.ZodEnum<{
39
+ low: "low";
40
+ normal: "normal";
41
+ urgent: "urgent";
42
+ }>;
43
+ sentiment: z.ZodEnum<{
44
+ angry: "angry";
45
+ frustrated: "frustrated";
46
+ neutral: "neutral";
47
+ positive: "positive";
48
+ }>;
49
+ confidence: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
50
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ classifiedAt: z.ZodOptional<z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>>>;
52
+ archived: z.ZodCodec<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodBoolean, z.ZodString]>, z.ZodBoolean>;
53
+ archivedAt: z.ZodOptional<z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>>>;
54
+ archivedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
+ messageCount: z.ZodNumber;
56
+ firstMessageAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
57
+ lastMessageAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
58
+ createdAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
59
+ updatedAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
60
+ }, z.core.$strip>;
61
+ export type SupportThread = z.output<typeof SupportThread>;
62
+ /**
63
+ * What a brand-new thread's classification columns hold before the Workflow has run.
64
+ *
65
+ * **`declaredCategory` is deliberately not in here**, and the omission is load-bearing rather than an
66
+ * oversight. This object is the set of columns the classifier owns — `runClassification` overwrites
67
+ * every one of them on each run — so a submitter's claim listed among them would be erased by the
68
+ * first classification and by every retry after it. It is set beside this spread, from the submission,
69
+ * and nothing on the classification path names it.
70
+ */
71
+ export declare const UNCLASSIFIED: {
72
+ readonly category: "uncategorized";
73
+ readonly priority: "normal";
74
+ readonly sentiment: "neutral";
75
+ readonly confidence: null;
76
+ readonly model: null;
77
+ readonly classifiedAt: null;
78
+ };
79
+ //# sourceMappingURL=thread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/data/thread.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4FmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;;uBAEb,QAAQ;wBACP,SAAS;;;;CAOrB,CAAC"}
@@ -0,0 +1,67 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { SupportAccountLinkSource, SupportChannel, SupportPriority, SupportSentiment, UNCATEGORIZED } from "./enums.js";
4
+ import { z } from "zod";
5
+ import { SQLiteBoolean, SQLiteDate } from "@pithy-sh/core/src/data/codecs";
6
+ //#region src/data/thread.ts
7
+ /**
8
+ * One conversation in `pithy_support_threads` — the row the inbox is a list of.
9
+ *
10
+ * **Everything on it is derived from immutable inbound mail.** The classification is recomputed, never
11
+ * repaired; the user link is a lookup, not a decision; the counters are facts about the messages
12
+ * below. There is no assignee and no status, because a status field is a state machine and a state
13
+ * machine attracts SLAs, escalation, and reporting — which is a ticketing product and explicitly not
14
+ * this. The two exceptions are deliberate and named: `archived` (one shared boolean meaning done) and
15
+ * the per-viewer flags, which live in their own table because nobody coordinates around them.
16
+ *
17
+ * The classification axes are **denormalized onto the thread** rather than joined from
18
+ * `pithy_support_classifications`. The whole dashboard interaction is "newest first, filtered by
19
+ * category and priority and archived", and that is a composite-index workload — a join to find the
20
+ * latest classification per thread would make the one query this table exists to serve the expensive
21
+ * one. The classification table keeps the history; this keeps the answer.
22
+ */
23
+ const SupportThread = z.object({
24
+ id: z.string().describe("UUID primary key. Text, not autoincrement, because a thread id reaches a dashboard and an enumerable inbox is an enumerable customer list."),
25
+ channel: SupportChannel.describe("How this conversation started — `email` at an inbound address, or `app` from a signed-in user. Never rewritten by a later message: the channel a thread opened on is what its account link's provenance follows from, and a thread that changed channel would change what its link means."),
26
+ inboxAddress: z.string().nullish().describe("The support address this thread arrived on, lowercased. Stored per thread so one Worker can serve several inboxes (support@, security@) and the dashboard can filter by which — and it is the address a reply comes back to. **Null on an `app` thread with no inbound address configured**, which is a supported deployment: a project can collect in-app feedback with no mail set up at all. Never null on an `email` thread, which by definition arrived at one."),
27
+ subject: z.string().describe("The subject of the message that opened the thread. Later replies never rewrite it — the thread keeps its name."),
28
+ fromAddress: z.string().describe("The sender's address, lowercased. The join key for the user link, the sender history, and the volume guard — so it is normalized on the way in, once. On an `app` thread it is the authenticated account's own email, read from the account rather than from anything the client sent, which is what lets a reply leave on the existing mail path with nothing new to configure."),
29
+ fromName: z.string().nullish().describe("The sender's display name from the `From` header, when they had one. Untrusted text; render it escaped."),
30
+ senderAuthenticated: SQLiteBoolean.describe("Whether the sender was proved to be who they claim. On an `email` thread that means the `From:` header was proved — DMARC passed, or SPF passed on an aligned domain — and **false does not mean forged**, it means unproven, which is the common case for a domain publishing no DMARC policy. On an `app` thread it is always true, and it is true for a stronger reason: there was no header to prove, because `requireAuth()` proved the session before the request reached a handler. It gates the customer link: an unproven sender is never decorated with somebody's real purchase history, because that is what turns a spoofed message into support-driven account takeover."),
31
+ userId: z.string().nullish().describe("The `pithy_auth_users.id` this sender resolves to, or null when the address belongs to nobody with an account — or when the sender was not authenticated, because an unproven `From:` must not resolve to a real customer. Derived on the mail path, so a customer who signs up later is linked by the next message rather than by a repair; on the app path it is the session's own user id and there is nothing to derive."),
32
+ accountLinkSource: SupportAccountLinkSource.nullish().describe("How `userId` was established, or null when there is no link. **The column that keeps a session-proven link from reading like a header-inferred one** — `senderAuthenticated` says whether to believe it, this says what was believed. A console rendering the two identically is the failure this exists to prevent, because the same operator action follows from very different evidence."),
33
+ declaredCategory: z.string().nullish().describe("**What the submitter said this is about** — a claim, made once, by the person writing. Null when nobody said: every `email` thread, and every `app` thread whose client offered no chooser. Written when the thread opens and **never afterwards**: not by a later message, and not by the classifier, which owns `category` and does not know this column exists. That separation is the whole point. A classification is recomputed on every retry, every manual reclassify, and every post-upgrade backfill — so a single column holding both facts loses whichever was written first, and the one an operator most needs is the *disagreement* between them: `declaredCategory: billing` beside `category: bug_report` says the customer thinks they were overcharged and the model thinks the app is broken, and no one column can say that. Validated against the effective taxonomy before it is stored, so this is never a client-writable vocabulary."),
34
+ category: z.string().describe("**What the classifier made of it** — the current answer, from this project's effective taxonomy, recomputed and never repaired. A plain string rather than an enum because the taxonomy is federated — the valid set is not known until an adopter composes the capability, so it is validated at the classification boundary instead of by the column. Distinct from `declaredCategory` above, which nothing here ever reads or writes."),
35
+ priority: SupportPriority.describe("The current priority — how fast this thread needs a human."),
36
+ sentiment: SupportSentiment.describe("The current sentiment — the churn signal."),
37
+ confidence: z.number().min(0).max(1).nullish().describe("The model's self-reported confidence in the current classification, 0..1. Null until a classification lands. Kept so a low-confidence inbox can be reviewed separately from a confident one."),
38
+ model: z.string().nullish().describe("The Workers AI model id that produced the current classification. Recorded so a reclassification pass after a model upgrade can tell which rows came from which model — the whole reason reclassification is possible at all."),
39
+ classifiedAt: SQLiteDate.nullish().describe("When the current classification was written; null until the Workflow has run."),
40
+ archived: SQLiteBoolean.describe("Done. One shared boolean, not per viewer, because if one person resolves a thread another should not still see it open. The mildest possible coordination state: no transitions to get wrong, nothing can be stuck, and unarchiving undoes a mistake instantly."),
41
+ archivedAt: SQLiteDate.nullish().describe("When the thread was last archived; null while it is open."),
42
+ archivedBy: z.string().nullish().describe("The control-plane subject that archived it. Recorded for the dashboard's convenience only — the answer to 'who marked this done' comes from the audit trail, which is why this is not an ownership column."),
43
+ messageCount: z.number().int().describe("How many messages the thread holds, inbound and outbound together."),
44
+ firstMessageAt: SQLiteDate.describe("When the thread opened — the receive time of its first message."),
45
+ lastMessageAt: SQLiteDate.describe("When the thread last moved. The inbox sorts on this, and it is the `receivedAt` half of every composite index and of the pagination cursor."),
46
+ createdAt: SQLiteDate.describe("When the thread row was created."),
47
+ updatedAt: SQLiteDate.describe("When the thread row was last written.")
48
+ }).describe("One support conversation in `pithy_support_threads` — derived entirely from the mail below it.");
49
+ /**
50
+ * What a brand-new thread's classification columns hold before the Workflow has run.
51
+ *
52
+ * **`declaredCategory` is deliberately not in here**, and the omission is load-bearing rather than an
53
+ * oversight. This object is the set of columns the classifier owns — `runClassification` overwrites
54
+ * every one of them on each run — so a submitter's claim listed among them would be erased by the
55
+ * first classification and by every retry after it. It is set beside this spread, from the submission,
56
+ * and nothing on the classification path names it.
57
+ */
58
+ const UNCLASSIFIED = {
59
+ category: UNCATEGORIZED,
60
+ priority: "normal",
61
+ sentiment: "neutral",
62
+ confidence: null,
63
+ model: null,
64
+ classifiedAt: null
65
+ };
66
+ //#endregion
67
+ export { SupportThread, UNCLASSIFIED };
@@ -0,0 +1,66 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { PithyError } from "@pithy-sh/core/src/error/pithyError";
4
+ import type { MessageParams } from "@pithy-sh/core/src/i18n/catalog";
5
+ /**
6
+ * `@pithy-sh/support` throw sugar. The `support/*` codes live in core's closed `KitErrorPayload` union
7
+ * (CLAUDE.md §Errors: capabilities add their codes to the one union); these subclasses are the
8
+ * package-local vehicles that set one of those members — the same pattern as `@pithy-sh/media` and
9
+ * `@pithy-sh/payments`. Runtime code in this package throws one of these, never a plain `new Error`.
10
+ *
11
+ * **Keep the public `message` free of anything the sender wrote.** This capability's entire input is
12
+ * attacker-controlled mail, so an error that echoed a subject line back would turn the error channel
13
+ * into a reflection surface. Sender text goes in `detail`, which the HTTP codec strips.
14
+ */
15
+ /** Variable parts each subclass accepts; `code`/`status` are fixed by the subclass. */
16
+ interface SupportErrorArgs {
17
+ /** Override the public, safe-to-expose message. */
18
+ message?: string;
19
+ /** A remediation hint (CLI action line). */
20
+ action?: string;
21
+ /** Internal context for logs + audit. Never serialized to clients. */
22
+ detail?: string;
23
+ /**
24
+ * Values a translating client interpolates into its own wording for this code. Client-facing, so —
25
+ * unlike `action` and `detail` — these cross the boundary with `message`.
26
+ */
27
+ params?: MessageParams;
28
+ }
29
+ /** A requested thread, message, or attachment does not exist. */
30
+ export declare class SupportNotFoundError extends PithyError {
31
+ constructor(args?: SupportErrorArgs, options?: {
32
+ cause?: unknown;
33
+ });
34
+ }
35
+ /** A category key or its description failed validation at author time. */
36
+ export declare class SupportInvalidCategoryError extends PithyError {
37
+ constructor(args?: SupportErrorArgs, options?: {
38
+ cause?: unknown;
39
+ });
40
+ }
41
+ /** An inbound message could not be parsed, or carried nothing this capability can store. */
42
+ export declare class SupportUnparseableMessageError extends PithyError {
43
+ constructor(args?: SupportErrorArgs, options?: {
44
+ cause?: unknown;
45
+ });
46
+ }
47
+ /** An inbound message was refused by the volume or size guard before anything was persisted. */
48
+ export declare class SupportRejectedError extends PithyError {
49
+ constructor(args?: SupportErrorArgs, options?: {
50
+ cause?: unknown;
51
+ });
52
+ }
53
+ /** The AI classification step failed, or the binding it needs is absent. */
54
+ export declare class SupportClassificationError extends PithyError {
55
+ constructor(args?: SupportErrorArgs, options?: {
56
+ cause?: unknown;
57
+ });
58
+ }
59
+ /** A reply could not be enqueued — the email capability is absent, or it refused the job. */
60
+ export declare class SupportReplyFailedError extends PithyError {
61
+ constructor(args?: SupportErrorArgs, options?: {
62
+ cause?: unknown;
63
+ });
64
+ }
65
+ export {};
66
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/error/errors.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE;;;;;;;;;GASG;AAEH,uFAAuF;AACvF,UAAU,gBAAgB;IACxB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,iEAAiE;AACjE,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF;AAED,0EAA0E;AAC1E,qBAAa,2BAA4B,SAAQ,UAAU;IACzD,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF;AAED,4FAA4F;AAC5F,qBAAa,8BAA+B,SAAQ,UAAU;IAC5D,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF;AAED,gGAAgG;AAChG,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF;AAED,4EAA4E;AAC5E,qBAAa,0BAA2B,SAAQ,UAAU;IACxD,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF;AAED,6FAA6F;AAC7F,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,YAAY,IAAI,GAAE,gBAAqB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYrE;CACF"}
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { PithyError } from "@pithy-sh/core/src/error/pithyError";
4
+ //#region src/error/errors.ts
5
+ /** A requested thread, message, or attachment does not exist. */
6
+ var SupportNotFoundError = class extends PithyError {
7
+ constructor(args = {}, options) {
8
+ super({
9
+ code: "support/not_found",
10
+ status: 404,
11
+ message: args.message ?? "That support thread does not exist.",
12
+ action: args.action,
13
+ detail: args.detail,
14
+ params: args.params
15
+ }, options);
16
+ }
17
+ };
18
+ /** A category key or its description failed validation at author time. */
19
+ var SupportInvalidCategoryError = class extends PithyError {
20
+ constructor(args = {}, options) {
21
+ super({
22
+ code: "support/invalid_category",
23
+ status: 400,
24
+ message: args.message ?? "That support category is not valid.",
25
+ action: args.action ?? "Use a lowercase snake_case key with one instructional sentence describing it.",
26
+ detail: args.detail,
27
+ params: args.params
28
+ }, options);
29
+ }
30
+ };
31
+ /** An inbound message could not be parsed, or carried nothing this capability can store. */
32
+ var SupportUnparseableMessageError = class extends PithyError {
33
+ constructor(args = {}, options) {
34
+ super({
35
+ code: "support/unparseable_message",
36
+ status: 400,
37
+ message: args.message ?? "That message could not be read as email.",
38
+ action: args.action,
39
+ detail: args.detail,
40
+ params: args.params
41
+ }, options);
42
+ }
43
+ };
44
+ /** An inbound message was refused by the volume or size guard before anything was persisted. */
45
+ var SupportRejectedError = class extends PithyError {
46
+ constructor(args = {}, options) {
47
+ super({
48
+ code: "support/rejected",
49
+ status: 429,
50
+ message: args.message ?? "That message was refused by the inbound guard.",
51
+ action: args.action,
52
+ detail: args.detail,
53
+ params: args.params
54
+ }, options);
55
+ }
56
+ };
57
+ /** The AI classification step failed, or the binding it needs is absent. */
58
+ var SupportClassificationError = class extends PithyError {
59
+ constructor(args = {}, options) {
60
+ super({
61
+ code: "support/classification_failed",
62
+ status: 500,
63
+ message: args.message ?? "Support classification could not complete.",
64
+ action: args.action,
65
+ detail: args.detail,
66
+ params: args.params
67
+ }, options);
68
+ }
69
+ };
70
+ /** A reply could not be enqueued — the email capability is absent, or it refused the job. */
71
+ var SupportReplyFailedError = class extends PithyError {
72
+ constructor(args = {}, options) {
73
+ super({
74
+ code: "support/reply_failed",
75
+ status: 502,
76
+ message: args.message ?? "That reply could not be sent.",
77
+ action: args.action ?? "Add the email capability and provision it, then retry.",
78
+ detail: args.detail,
79
+ params: args.params
80
+ }, options);
81
+ }
82
+ };
83
+ //#endregion
84
+ export { SupportClassificationError, SupportInvalidCategoryError, SupportNotFoundError, SupportRejectedError, SupportReplyFailedError, SupportUnparseableMessageError };
@@ -0,0 +1,46 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
4
+ import type { MiddlewareHandler } from "hono";
5
+ /**
6
+ * Support's identity gates: the copied `requireAuth()` its in-app submission routes wear, and the
7
+ * control-plane gate its management routes take from core's seam.
8
+ *
9
+ * **The scope constants moved to `./scopes`** (#315). A management client reads them to render what a
10
+ * connection may do, and it reads them in a browser — so they cannot live in a module that imports
11
+ * Hono middleware and `PithyHonoEnv`. The gates stayed here; the names they demand are next door, and
12
+ * the routes import both.
13
+ *
14
+ * ## Two surfaces, and the split is the whole shape of this file
15
+ *
16
+ * **The management surface is `control-plane` and default-denied.** It reads and acts on every
17
+ * customer's private correspondence, so it answers to a credential the adopter issued and to nothing
18
+ * else. The seam's gate is imported from core rather than copied — core is a hard dependency of every
19
+ * capability, so importing its gate cannot leave a deployment without one, and with the seam
20
+ * uncomposed `requireControlPlane` raises `controlplane/not_connected` rather than passing.
21
+ *
22
+ * **The submission surface is `bearer`/`session` and is the adopter's own signed-in user.** It exists
23
+ * because a product with a logged-in user, a session, and a support console should not have to ask
24
+ * that user to open their mail client — and because the hardest problem on the mail path, proving a
25
+ * `From:` header, does not exist on a request whose session was already proved.
26
+ *
27
+ * **`requireAuth()` never appears on a management route, and `requireControlPlane` never appears on a
28
+ * submission route.** They are not two strengths of the same gate. A management client holds no
29
+ * session and owns no account row — core leaves `c.var.auth` null for one deliberately — so stacking
30
+ * them would deny every legitimate call on both surfaces, permanently, with no credential able to fix
31
+ * it. What each route may *see* follows from which gate it wears: an operator reads a thread with its
32
+ * classification and its sender's purchases, and a submitter reads their own words back and nothing
33
+ * else.
34
+ */
35
+ /**
36
+ * Require an authenticated caller — core's `AuthContext` seam, and the gate on every in-app route.
37
+ *
38
+ * **Copied from `@pithy-sh/auth`, not imported, and the duplication is deliberate.** Importing it would
39
+ * make auth a hard dependency, and *a package that imports its authorization from another package
40
+ * fails open when that package is absent*. Depending on the core seam instead means `c.var.auth` is
41
+ * simply null with no auth capability composed, and every submission route denies. Failing closed is
42
+ * not a side effect of the copy; it is the reason for it — and on this capability the thing behind the
43
+ * gate is somebody's support history.
44
+ */
45
+ export declare function requireAuth(): MiddlewareHandler<PithyHonoEnv>;
46
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/http/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAU7D"}
@@ -0,0 +1,55 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { UnauthorizedError } from "@pithy-sh/core/src/error/pithyError";
4
+ //#region src/http/guards.ts
5
+ /**
6
+ * Support's identity gates: the copied `requireAuth()` its in-app submission routes wear, and the
7
+ * control-plane gate its management routes take from core's seam.
8
+ *
9
+ * **The scope constants moved to `./scopes`** (#315). A management client reads them to render what a
10
+ * connection may do, and it reads them in a browser — so they cannot live in a module that imports
11
+ * Hono middleware and `PithyHonoEnv`. The gates stayed here; the names they demand are next door, and
12
+ * the routes import both.
13
+ *
14
+ * ## Two surfaces, and the split is the whole shape of this file
15
+ *
16
+ * **The management surface is `control-plane` and default-denied.** It reads and acts on every
17
+ * customer's private correspondence, so it answers to a credential the adopter issued and to nothing
18
+ * else. The seam's gate is imported from core rather than copied — core is a hard dependency of every
19
+ * capability, so importing its gate cannot leave a deployment without one, and with the seam
20
+ * uncomposed `requireControlPlane` raises `controlplane/not_connected` rather than passing.
21
+ *
22
+ * **The submission surface is `bearer`/`session` and is the adopter's own signed-in user.** It exists
23
+ * because a product with a logged-in user, a session, and a support console should not have to ask
24
+ * that user to open their mail client — and because the hardest problem on the mail path, proving a
25
+ * `From:` header, does not exist on a request whose session was already proved.
26
+ *
27
+ * **`requireAuth()` never appears on a management route, and `requireControlPlane` never appears on a
28
+ * submission route.** They are not two strengths of the same gate. A management client holds no
29
+ * session and owns no account row — core leaves `c.var.auth` null for one deliberately — so stacking
30
+ * them would deny every legitimate call on both surfaces, permanently, with no credential able to fix
31
+ * it. What each route may *see* follows from which gate it wears: an operator reads a thread with its
32
+ * classification and its sender's purchases, and a submitter reads their own words back and nothing
33
+ * else.
34
+ */
35
+ /**
36
+ * Require an authenticated caller — core's `AuthContext` seam, and the gate on every in-app route.
37
+ *
38
+ * **Copied from `@pithy-sh/auth`, not imported, and the duplication is deliberate.** Importing it would
39
+ * make auth a hard dependency, and *a package that imports its authorization from another package
40
+ * fails open when that package is absent*. Depending on the core seam instead means `c.var.auth` is
41
+ * simply null with no auth capability composed, and every submission route denies. Failing closed is
42
+ * not a side effect of the copy; it is the reason for it — and on this capability the thing behind the
43
+ * gate is somebody's support history.
44
+ */
45
+ function requireAuth() {
46
+ return async (c, next) => {
47
+ if (!c.var.auth) throw new UnauthorizedError({
48
+ message: "Authentication required.",
49
+ action: "Sign in and retry with a valid session or bearer token."
50
+ });
51
+ await next();
52
+ };
53
+ }
54
+ //#endregion
55
+ export { requireAuth };
@@ -0,0 +1,91 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { D1Database, R2Bucket } from "@cloudflare/workers-types";
4
+ import type { AuditEmit } from "@pithy-sh/core/src/audit/recorder";
5
+ import type { Logger } from "@pithy-sh/core/src/logger/logger";
6
+ import type { ObjectStore } from "@pithy-sh/storage/src/object/store";
7
+ import type { SupportConfig } from "../config/config";
8
+ import type { SupportCategories } from "../data/categories";
9
+ import type { SupportDatabase } from "../data/tables";
10
+ import { sendReply } from "../reply/send";
11
+ import type { SupportReplySnippets } from "../reply/snippets";
12
+ import type { SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportReplySentResponse, SupportReplyView, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse, SupportThreadView } from "./responses";
13
+ import type { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput } from "./schemas";
14
+ /**
15
+ * The support handlers — every one of them behind the `control-plane` gate, and every one of them
16
+ * taking already-validated values.
17
+ *
18
+ * Nothing here reads raw request input: the route line carries the contract and the handler takes
19
+ * typed arguments, which is what lets these be tested without a request at all.
20
+ */
21
+ /** Everything the handlers need, resolved per request. */
22
+ export interface HandlerDeps {
23
+ /** The support tables. */
24
+ db: SupportDatabase;
25
+ /** The raw `DB` binding — the linkage reads sibling capabilities' tables through it. */
26
+ d1: D1Database;
27
+ /** The resolved config. */
28
+ config: SupportConfig;
29
+ /** The effective taxonomy. */
30
+ categories: SupportCategories;
31
+ /** The effective canned-reply catalog. */
32
+ snippets: SupportReplySnippets;
33
+ /** Whether the FTS5 index is composed. */
34
+ fts: boolean;
35
+ /** The attachment bucket, when one is bound. */
36
+ bucket?: R2Bucket;
37
+ /** The object store attachment URLs are signed through, when credentials are available. */
38
+ store?: ObjectStore;
39
+ /** The email capability's env-bound `enqueue`, when email is composed. */
40
+ enqueue?: Parameters<typeof sendReply>[0]["enqueue"];
41
+ /** Start a classification for a message. Resolves to whether an instance actually started. */
42
+ dispatchClassify: (messageId: string) => Promise<boolean>;
43
+ /** The audit seam. */
44
+ emit: AuditEmit;
45
+ /** The request logger. */
46
+ log: Logger;
47
+ /** Generate a row id. */
48
+ newId: () => string;
49
+ /** Now. */
50
+ now: () => Date;
51
+ }
52
+ /** List the inbox. */
53
+ export declare function listInbox(deps: HandlerDeps, query: ListThreadsQuery, viewer: string): Promise<SupportThreadsResponse>;
54
+ /** Read one conversation in full. */
55
+ export declare function readConversation(deps: HandlerDeps, threadId: string): Promise<SupportThreadResponse>;
56
+ /** Mark a conversation done, or reopen it. */
57
+ export declare function archiveConversation(deps: HandlerDeps, threadId: string, input: ArchiveThreadInput, viewer: string): Promise<SupportThreadView>;
58
+ /** Answer the customer. */
59
+ export declare function replyToConversation(deps: HandlerDeps, threadId: string, input: ReplyInput, viewer: string): Promise<SupportReplySentResponse>;
60
+ /** Re-run the classifier over a conversation's latest inbound message. */
61
+ export declare function reclassifyConversation(deps: HandlerDeps, threadId: string, viewer: string): Promise<SupportReclassifiedResponse>;
62
+ /** Set one viewer's private flags. */
63
+ export declare function updateFlags(deps: HandlerDeps, threadId: string, input: FlagsInput, viewer: string): Promise<SupportFlagsResponse>;
64
+ /** The canned reply catalog. */
65
+ export declare function listReplies(deps: HandlerDeps, query: RepliesQuery): SupportReplyView[];
66
+ /**
67
+ * Take an in-app support request from a signed-in user.
68
+ *
69
+ * `userId` is a parameter rather than something read out of `input`, and that is the contract this
70
+ * whole channel rests on: it comes from `c.var.auth`, which `requireAuth()` filled before the route's
71
+ * validator ran. Nothing in the submitted body names an account, and the schema has no field that
72
+ * could.
73
+ *
74
+ * The configured length bounds are applied here rather than in the schema, for the reason
75
+ * `schemas.ts` states: a request schema is built once at module load and the adopter's numbers are
76
+ * resolved per project. This is config-backed resolution, which belongs in a handler — the same rule
77
+ * that keeps `board()` and `currency()` lookups out of a param schema.
78
+ */
79
+ export declare function submitFeedbackRequest(deps: HandlerDeps, input: SubmitFeedbackInput, userId: string): Promise<SupportSubmissionResponse>;
80
+ /** List the caller's own in-app conversations. */
81
+ export declare function listMyThreads(deps: HandlerDeps, query: MyThreadsQuery, userId: string): Promise<SupportMyThreadsResponse>;
82
+ /**
83
+ * Read one of the caller's own conversations.
84
+ *
85
+ * The scoping is `readOwnThread`'s `where`, so a thread belonging to somebody else raises the same
86
+ * `support/not_found` as one that never existed. The projection is `myThreadView`/`myMessageView`,
87
+ * built from scratch rather than by omitting fields from the operator's — see `views.ts` for why that
88
+ * distinction is the security boundary rather than a style preference.
89
+ */
90
+ export declare function readMyThread(deps: HandlerDeps, threadId: string, userId: string): Promise<SupportMyThreadResponse>;
91
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"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"}