@pithy-sh/support 0.1.2 → 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,7 @@
1
+ //#region src/version.generated.ts
2
+ /** This package's npm name — the join key against a release feed. */
3
+ const PACKAGE_NAME = "@pithy-sh/support";
4
+ /** This package's version, stamped from its own package.json at generation time. */
5
+ const PACKAGE_VERSION = "0.1.3";
6
+ //#endregion
7
+ export { PACKAGE_NAME, PACKAGE_VERSION };
@@ -0,0 +1,63 @@
1
+ import type { SupportAi } from "../ai/classify";
2
+ import type { SupportAiConfig } from "../config/config";
3
+ import type { SupportCategories } from "../data/categories";
4
+ import { type SupportDatabase } from "../data/tables";
5
+ /**
6
+ * The classification orchestration — the pure, testable core the Workflow step runs.
7
+ *
8
+ * Reads a message, asks the model about it, **appends** a classification row, and denormalizes the
9
+ * answer onto its thread. The two writes are deliberate and asymmetric: the history is append-only
10
+ * so a model upgrade is visible in the data rather than a silent rewrite, and the thread carries only
11
+ * the current answer because that is what the inbox query reads.
12
+ *
13
+ * Idempotent by construction. Re-running over the same message appends a second row and overwrites
14
+ * the same three thread columns with a fresh judgment — which is what makes a Workflow retry, a
15
+ * manual reclassify, and a post-upgrade backfill the same operation.
16
+ */
17
+ /** Everything classification needs, all injectable. */
18
+ export interface ClassifyDeps {
19
+ /** The support tables. */
20
+ db: SupportDatabase;
21
+ /** The Workers AI binding. */
22
+ ai: SupportAi;
23
+ /** The effective taxonomy — the defaults plus the adopter's. */
24
+ categories: SupportCategories;
25
+ /** Model, bounds, and temperature. */
26
+ ai_config: SupportAiConfig;
27
+ /**
28
+ * Archive a thread the classifier calls `spam`, so it never reaches the open inbox.
29
+ *
30
+ * **Archived, never deleted.** The classifier is wrong sometimes, and a pass that destroyed mail
31
+ * would be untrustworthy the first time it was — whereas an archived thread is still readable under
32
+ * the archived filter and one click from being back. That is what makes `spam` a filter rather than
33
+ * a bin, and it is why this is safe to have on by default.
34
+ */
35
+ archiveSpam: boolean;
36
+ /** Generate a row id. */
37
+ newId: () => string;
38
+ /** Now. */
39
+ now: () => Date;
40
+ }
41
+ /** What one classification run did. `null` when the message no longer exists. */
42
+ export interface ClassifyOutcome {
43
+ /** The thread the classification landed on. */
44
+ threadId: string;
45
+ /** The category chosen. */
46
+ category: string;
47
+ /** The model's confidence. */
48
+ confidence: number;
49
+ /** The model that produced it. */
50
+ model: string;
51
+ /** Whether this run archived the thread because it classified as spam. */
52
+ archivedAsSpam: boolean;
53
+ }
54
+ /** Classify one stored message and write the result. Returns null when the message is gone. */
55
+ export declare function runClassification(deps: ClassifyDeps, messageId: string): Promise<ClassifyOutcome | null>;
56
+ /**
57
+ * The message a manual reclassify runs against: a thread's most recent **inbound** message.
58
+ *
59
+ * Inbound, not latest — a thread whose last message is our own reply would otherwise be reclassified
60
+ * on the text the adopter wrote, which says nothing about what the customer wanted.
61
+ */
62
+ export declare function latestInboundMessageId(db: SupportDatabase, threadId: string): Promise<string | undefined>;
63
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/workflows/classify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;;GAWG;AAEH,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,8BAA8B;IAC9B,EAAE,EAAE,SAAS,CAAC;IACd,gEAAgE;IAChE,UAAU,EAAE,iBAAiB,CAAC;IAC9B,sCAAsC;IACtC,SAAS,EAAE,eAAe,CAAC;IAC3B;;;;;;;OAOG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,+FAA+F;AAC/F,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAwE9G;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAc/G"}
@@ -0,0 +1,68 @@
1
+ import "../data/enums.js";
2
+ import { SupportClassification } from "../data/classification.js";
3
+ import { SUPPORT_CLASSIFICATIONS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_THREADS_TABLE } from "../data/tables.js";
4
+ import { classifyMessage } from "../ai/classify.js";
5
+ //#region src/workflows/classify.ts
6
+ /** Classify one stored message and write the result. Returns null when the message is gone. */
7
+ async function runClassification(deps, messageId) {
8
+ const message = await deps.db.selectFrom(SUPPORT_MESSAGES_TABLE).select([
9
+ "id",
10
+ "threadId",
11
+ "subject",
12
+ "textBody"
13
+ ]).where("id", "=", messageId).executeTakeFirst();
14
+ if (!message) return null;
15
+ const result = await classifyMessage(deps.ai, {
16
+ categories: deps.categories,
17
+ subject: message.subject,
18
+ body: message.textBody,
19
+ model: deps.ai_config.model,
20
+ maxChars: deps.ai_config.maxChars,
21
+ temperature: deps.ai_config.temperature
22
+ });
23
+ const now = deps.now();
24
+ await deps.db.insertInto(SUPPORT_CLASSIFICATIONS_TABLE).values(SupportClassification.encode({
25
+ id: deps.newId(),
26
+ threadId: message.threadId,
27
+ messageId: message.id,
28
+ category: result.category,
29
+ priority: result.priority,
30
+ sentiment: result.sentiment,
31
+ confidence: result.confidence,
32
+ model: result.model,
33
+ createdAt: now
34
+ })).execute();
35
+ const archivedAsSpam = deps.archiveSpam && result.category === "spam";
36
+ await deps.db.updateTable(SUPPORT_THREADS_TABLE).set({
37
+ category: result.category,
38
+ priority: result.priority,
39
+ sentiment: result.sentiment,
40
+ confidence: result.confidence,
41
+ model: result.model,
42
+ classifiedAt: now.getTime(),
43
+ updatedAt: now.getTime(),
44
+ ...archivedAsSpam ? {
45
+ archived: 1,
46
+ archivedAt: now.getTime(),
47
+ archivedBy: result.model
48
+ } : {}
49
+ }).where("id", "=", message.threadId).execute();
50
+ return {
51
+ threadId: message.threadId,
52
+ category: result.category,
53
+ confidence: result.confidence,
54
+ model: result.model,
55
+ archivedAsSpam
56
+ };
57
+ }
58
+ /**
59
+ * The message a manual reclassify runs against: a thread's most recent **inbound** message.
60
+ *
61
+ * Inbound, not latest — a thread whose last message is our own reply would otherwise be reclassified
62
+ * on the text the adopter wrote, which says nothing about what the customer wanted.
63
+ */
64
+ async function latestInboundMessageId(db, threadId) {
65
+ return (await db.selectFrom(SUPPORT_MESSAGES_TABLE).select(["id"]).where("threadId", "=", threadId).where("direction", "=", "inbound").orderBy("receivedAt", "desc").orderBy("id", "desc").limit(1).executeTakeFirst())?.id;
66
+ }
67
+ //#endregion
68
+ export { latestInboundMessageId, runClassification };
@@ -0,0 +1,39 @@
1
+ import type { WorkflowRetryPolicy } from "@pithy-sh/core/src/workflow/faults";
2
+ /**
3
+ * **What a classification retries, and what it refuses to.**
4
+ *
5
+ * `ai/classify.ts` already drew this line and drew it correctly: a model that returns prose, an invented
6
+ * label, or an envelope nobody documented yields `uncategorized` at zero confidence and never throws,
7
+ * because throwing would burn a retry budget on a model that is going to say the same thing next time.
8
+ * The one case it deliberately does not swallow is a binding that is *unavailable*. This is that
9
+ * sentence in the form the durable step reads (pithy-sh/pithy#348).
10
+ *
11
+ * ## Retryable, and why
12
+ *
13
+ * - **`core/upstream_failed`** — Workers AI rejected the call. Raised at exactly one place, the wrap
14
+ * around `ai.run` in `classifyMessage`, and it means the model did not answer at all. That is the
15
+ * whole population of transient faults in a classification, and the code is what lets the step tell
16
+ * it from a bad answer — a raw throw would be `unclassified`, and unclassified is terminal.
17
+ * - **A transient D1 fault** — the message read, the appended history row, the thread denormalization.
18
+ * Classified in core by `withD1Retry`, never restated here.
19
+ *
20
+ * ## Terminal, and why
21
+ *
22
+ * - **`support/not_found`** — a thread or message that is gone. `runClassification` does not even raise
23
+ * it for the message it was started for: a missing row returns `null`, because an instance outliving
24
+ * the row that started it is the ordinary way a rollback looks.
25
+ * - **`support/classification_failed`** — the AI binding is absent from the env. A binding does not
26
+ * appear on the fourth attempt; this wants `pithy support provision`.
27
+ * - **`support/invalid_category`** — the adopter's taxonomy will not validate. Config, and identical
28
+ * next time.
29
+ * - **`support/unparseable_message`, `support/rejected`, `support/reply_failed`** — all inbound-path and
30
+ * reply-path refusals. None runs inside the classify step.
31
+ * - **`validation/invalid_input`** — a payload the schema refuses.
32
+ *
33
+ * **Re-running is free, which is what makes the retryable entry safe.** Classification is idempotent by
34
+ * construction: a second pass appends a second history row and overwrites the same three thread columns
35
+ * with a fresh judgment, which is why a Workflow retry, a manual reclassify, and a post-upgrade
36
+ * backfill are the same operation.
37
+ */
38
+ export declare const supportWorkflowRetry: WorkflowRetryPolicy;
39
+ //# sourceMappingURL=retryPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retryPolicy.d.ts","sourceRoot":"","sources":["../../src/workflows/retryPolicy.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,EAAE,mBAMlC,CAAC"}
@@ -0,0 +1,43 @@
1
+ //#region src/workflows/retryPolicy.ts
2
+ /**
3
+ * **What a classification retries, and what it refuses to.**
4
+ *
5
+ * `ai/classify.ts` already drew this line and drew it correctly: a model that returns prose, an invented
6
+ * label, or an envelope nobody documented yields `uncategorized` at zero confidence and never throws,
7
+ * because throwing would burn a retry budget on a model that is going to say the same thing next time.
8
+ * The one case it deliberately does not swallow is a binding that is *unavailable*. This is that
9
+ * sentence in the form the durable step reads (pithy-sh/pithy#348).
10
+ *
11
+ * ## Retryable, and why
12
+ *
13
+ * - **`core/upstream_failed`** — Workers AI rejected the call. Raised at exactly one place, the wrap
14
+ * around `ai.run` in `classifyMessage`, and it means the model did not answer at all. That is the
15
+ * whole population of transient faults in a classification, and the code is what lets the step tell
16
+ * it from a bad answer — a raw throw would be `unclassified`, and unclassified is terminal.
17
+ * - **A transient D1 fault** — the message read, the appended history row, the thread denormalization.
18
+ * Classified in core by `withD1Retry`, never restated here.
19
+ *
20
+ * ## Terminal, and why
21
+ *
22
+ * - **`support/not_found`** — a thread or message that is gone. `runClassification` does not even raise
23
+ * it for the message it was started for: a missing row returns `null`, because an instance outliving
24
+ * the row that started it is the ordinary way a rollback looks.
25
+ * - **`support/classification_failed`** — the AI binding is absent from the env. A binding does not
26
+ * appear on the fourth attempt; this wants `pithy support provision`.
27
+ * - **`support/invalid_category`** — the adopter's taxonomy will not validate. Config, and identical
28
+ * next time.
29
+ * - **`support/unparseable_message`, `support/rejected`, `support/reply_failed`** — all inbound-path and
30
+ * reply-path refusals. None runs inside the classify step.
31
+ * - **`validation/invalid_input`** — a payload the schema refuses.
32
+ *
33
+ * **Re-running is free, which is what makes the retryable entry safe.** Classification is idempotent by
34
+ * construction: a second pass appends a second history row and overwrites the same three thread columns
35
+ * with a fresh judgment, which is why a Workflow retry, a manual reclassify, and a post-upgrade
36
+ * backfill are the same operation.
37
+ */
38
+ const supportWorkflowRetry = {
39
+ capability: "support",
40
+ retryable: { "core/upstream_failed": "The model did not answer at all; classification is idempotent, so asking again costs one call." }
41
+ };
42
+ //#endregion
43
+ export { supportWorkflowRetry };
@@ -0,0 +1,43 @@
1
+ import type { WorkflowRegistry } from "@pithy-sh/core/src/workflow/spec";
2
+ import { z } from "zod";
3
+ /**
4
+ * The one durable job support owns, declared once.
5
+ *
6
+ * **Classification is a Workflow, not part of the `email()` handler**, and that is a hard constraint
7
+ * rather than a preference. An inbound handler runs under a tight CPU budget; a model call is
8
+ * hundreds of milliseconds of wall time it cannot spend, and a model that is slow or briefly
9
+ * unavailable would take the *persistence* of the message down with it. So the handler does the
10
+ * cheap, must-not-fail half — parse, bound, store — and hands off. A classification that fails
11
+ * retries against a message that is already safely in D1.
12
+ *
13
+ * `optional: true`: the Workflow lives in the prebuilt support worker that `pithy support provision`
14
+ * deploys, so a project that has not provisioned must still boot and still receive mail. An absent
15
+ * binding degrades to a logged skip and a thread that stays `uncategorized` — which is exactly the
16
+ * state a classification is allowed to be in.
17
+ */
18
+ /** The capability name — the first segment of the dispatch key and of every deployed workflow name. */
19
+ export declare const SUPPORT_CAPABILITY = "support";
20
+ /** Which message to classify. */
21
+ export declare const SupportClassifyParams: z.ZodObject<{
22
+ messageId: z.ZodString;
23
+ }, z.core.$strip>;
24
+ export type SupportClassifyParams = z.infer<typeof SupportClassifyParams>;
25
+ /** Support's durable jobs, keyed by job name. The key is the second segment of the `support/<job>` dispatch key. */
26
+ export declare const supportWorkflows: {
27
+ readonly classify: {
28
+ readonly binding: "SUPPORT_CLASSIFY";
29
+ readonly className: "SupportClassifyWorkflow";
30
+ readonly params: z.ZodObject<{
31
+ messageId: z.ZodString;
32
+ }, z.core.$strip>;
33
+ readonly optional: true;
34
+ };
35
+ };
36
+ /**
37
+ * Support's jobs as a dispatch registry, keyed `support/<job>`. Built here rather than through
38
+ * `composeWorkflows` because the inbound handler dispatches from inside the capability, before any
39
+ * project-wide registry exists — and the key format comes from core's {@link workflowKey} either
40
+ * way, so the two cannot drift.
41
+ */
42
+ export declare const supportWorkflowRegistry: WorkflowRegistry;
43
+ //# sourceMappingURL=specs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs.d.ts","sourceRoot":"","sources":["../../src/workflows/specs.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,kCAAkC,CAAC;AAC1F,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;GAcG;AAEH,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C,iCAAiC;AACjC,eAAO,MAAM,qBAAqB;;iBASsE,CAAC;AACzG,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,oHAAoH;AACpH,eAAO,MAAM,gBAAgB;;iBAEzB,OAAO,EAAE,kBAAkB;iBAC3B,SAAS,EAAE,yBAAyB;iBACpC,MAAM;;;iBACN,QAAQ;;CAEwB,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAKrC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { workflowKey } from "@pithy-sh/core/src/workflow/naming";
3
+ //#region src/workflows/specs.ts
4
+ /**
5
+ * The one durable job support owns, declared once.
6
+ *
7
+ * **Classification is a Workflow, not part of the `email()` handler**, and that is a hard constraint
8
+ * rather than a preference. An inbound handler runs under a tight CPU budget; a model call is
9
+ * hundreds of milliseconds of wall time it cannot spend, and a model that is slow or briefly
10
+ * unavailable would take the *persistence* of the message down with it. So the handler does the
11
+ * cheap, must-not-fail half — parse, bound, store — and hands off. A classification that fails
12
+ * retries against a message that is already safely in D1.
13
+ *
14
+ * `optional: true`: the Workflow lives in the prebuilt support worker that `pithy support provision`
15
+ * deploys, so a project that has not provisioned must still boot and still receive mail. An absent
16
+ * binding degrades to a logged skip and a thread that stays `uncategorized` — which is exactly the
17
+ * state a classification is allowed to be in.
18
+ */
19
+ /** The capability name — the first segment of the dispatch key and of every deployed workflow name. */
20
+ const SUPPORT_CAPABILITY = "support";
21
+ /** Which message to classify. */
22
+ const SupportClassifyParams = z.object({ messageId: z.string().min(1).describe("The `pithy_support_messages.id` to classify. The Workflow reads that message, asks the model about it, appends a classification row, and denormalizes the answer onto its thread.") }).describe("The instance parameters of the classification Workflow — the one message it runs against.");
23
+ /** Support's durable jobs, keyed by job name. The key is the second segment of the `support/<job>` dispatch key. */
24
+ const supportWorkflows = { classify: {
25
+ binding: "SUPPORT_CLASSIFY",
26
+ className: "SupportClassifyWorkflow",
27
+ params: SupportClassifyParams,
28
+ optional: true
29
+ } };
30
+ /**
31
+ * Support's jobs as a dispatch registry, keyed `support/<job>`. Built here rather than through
32
+ * `composeWorkflows` because the inbound handler dispatches from inside the capability, before any
33
+ * project-wide registry exists — and the key format comes from core's {@link workflowKey} either
34
+ * way, so the two cannot drift.
35
+ */
36
+ const supportWorkflowRegistry = Object.fromEntries(Object.entries(supportWorkflows).map(([job, spec]) => {
37
+ const key = workflowKey(SUPPORT_CAPABILITY, job);
38
+ return [key, {
39
+ key,
40
+ capability: SUPPORT_CAPABILITY,
41
+ job,
42
+ spec
43
+ }];
44
+ }));
45
+ //#endregion
46
+ export { SUPPORT_CAPABILITY, SupportClassifyParams, supportWorkflowRegistry, supportWorkflows };
@@ -0,0 +1,48 @@
1
+ import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
2
+ import type { D1Database } from "@cloudflare/workers-types";
3
+ import type { SupportAi } from "../ai/classify";
4
+ /**
5
+ * The prebuilt support worker. `pithy support provision` deploys one per environment; the adopter
6
+ * authors no code for it. It hosts the classification Workflow — a thin durable shell around the
7
+ * tested orchestration in `classify.ts`. The app worker receives the mail, stores it, and starts an
8
+ * instance.
9
+ *
10
+ * This module imports `cloudflare:workers`, so it runs only in the Workers runtime and is excluded
11
+ * from the node `.describe()` meta-test.
12
+ *
13
+ * **The effective taxonomy arrives as config, not as code.** `SUPPORT_CONFIG` carries the adopter's
14
+ * own categories, so their `tournament_dispute` reaches the prompt without this worker importing
15
+ * anything of theirs — which it could not do anyway, since it is deployed from this package.
16
+ *
17
+ * **The default export is what makes this an ES module** (#426). It exports one Workflow class and has
18
+ * no cron, so until now it exported no default — and wrangler infers a worker's module format from
19
+ * exactly that, so the build read it as a service worker and refused `cloudflare:workers` outright.
20
+ * The host did not build, `pithy dev` carried on past it, and classification silently never ran. The
21
+ * refusal it exports is the honest body for a host with no request surface; see
22
+ * `@pithy-sh/core/src/workflow/hostEntry`.
23
+ */
24
+ /** The support worker's env: the app database, the AI binding, and the serialized config. */
25
+ export interface SupportWorkerEnv {
26
+ /** The app database the support tables live in. */
27
+ DB: D1Database;
28
+ /** The Workers AI binding. */
29
+ AI: SupportAi;
30
+ /** The resolved support config as a JSON string, filled at provision. */
31
+ SUPPORT_CONFIG?: string;
32
+ }
33
+ /** Classify one stored message and write the result. */
34
+ export declare class SupportClassifyWorkflow extends WorkflowEntrypoint<SupportWorkerEnv, {
35
+ messageId: string;
36
+ }> {
37
+ run(event: WorkflowEvent<{
38
+ messageId: string;
39
+ }>, step: WorkflowStep): Promise<void>;
40
+ }
41
+ /**
42
+ * The module's default export, and therefore its format. See `hostEntry` for why a Workflow host needs
43
+ * one at all, and why this one refuses rather than being empty: nothing reaches this worker over HTTP —
44
+ * the app worker stores the message and starts an instance on the `SUPPORT_CLASSIFY` binding.
45
+ */
46
+ declare const _default: import("@pithy-sh/core/src/workflow/hostEntry").WorkflowHostEntry;
47
+ export default _default;
48
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/workflows/worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQhD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,EAAE,EAAE,UAAU,CAAC;IACf,8BAA8B;IAC9B,EAAE,EAAE,SAAS,CAAC;IACd,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wDAAwD;AACxD,qBAAa,uBAAwB,SAAQ,kBAAkB,CAAC,gBAAgB,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;IACvF,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBjG;CACF;AAED;;;;GAIG"}
@@ -0,0 +1,37 @@
1
+ import { SupportConfig } from "../config/config.js";
2
+ import { resolveCategories } from "../data/categories.js";
3
+ import { supportDatabase } from "../data/tables.js";
4
+ import { SUPPORT_CAPABILITY } from "./specs.js";
5
+ import { runClassification } from "./classify.js";
6
+ import { supportWorkflowRetry } from "./retryPolicy.js";
7
+ import { WorkflowEntrypoint } from "cloudflare:workers";
8
+ import { NonRetryableError } from "cloudflare:workflows";
9
+ import { classifiedSteps } from "@pithy-sh/core/src/workflow/faults";
10
+ import { workflowHostEntry } from "@pithy-sh/core/src/workflow/hostEntry";
11
+ //#region src/workflows/worker.ts
12
+ /** Classify one stored message and write the result. */
13
+ var SupportClassifyWorkflow = class extends WorkflowEntrypoint {
14
+ async run(event, step) {
15
+ const config = SupportConfig.parse(this.env.SUPPORT_CONFIG ? JSON.parse(this.env.SUPPORT_CONFIG) : {});
16
+ const deps = {
17
+ db: supportDatabase(this.env.DB),
18
+ ai: this.env.AI,
19
+ categories: resolveCategories(config.categories),
20
+ ai_config: config.ai,
21
+ archiveSpam: config.guard.archiveSpam,
22
+ newId: () => crypto.randomUUID(),
23
+ now: () => /* @__PURE__ */ new Date()
24
+ };
25
+ await classifiedSteps(step, supportWorkflowRetry, NonRetryableError).do(`classify-${event.payload.messageId}`, async () => {
26
+ await runClassification(deps, event.payload.messageId);
27
+ });
28
+ }
29
+ };
30
+ /**
31
+ * The module's default export, and therefore its format. See `hostEntry` for why a Workflow host needs
32
+ * one at all, and why this one refuses rather than being empty: nothing reaches this worker over HTTP —
33
+ * the app worker stores the message and starts an instance on the `SUPPORT_CLASSIFY` binding.
34
+ */
35
+ var worker_default = workflowHostEntry(SUPPORT_CAPABILITY);
36
+ //#endregion
37
+ export { SupportClassifyWorkflow, worker_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/support",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,19 +8,24 @@
8
8
  "directory": "packages/support"
9
9
  },
10
10
  "files": [
11
+ "dist",
11
12
  "src",
12
13
  "pithy.manifest.json",
13
- "!src/**/*.test.*"
14
+ "!src/**/*.test.*",
15
+ "!dist/**/*.test.*"
14
16
  ],
15
17
  "type": "module",
16
18
  "engines": {
17
19
  "node": ">=22"
18
20
  },
19
21
  "exports": {
20
- "./src/*": "./src/*.ts"
22
+ "./src/*": {
23
+ "types": "./dist/*.d.ts",
24
+ "default": "./dist/*.js"
25
+ }
21
26
  },
22
27
  "scripts": {
23
- "build": "tsc -p tsconfig.json --noEmit false --outDir dist",
28
+ "build": "tsdown && tsc -p tsconfig.build.json",
24
29
  "typecheck": "tsc -p tsconfig.json",
25
30
  "test": "vitest run",
26
31
  "test:node": "vitest run --project=node",
@@ -31,19 +36,19 @@
31
36
  "dependencies": {
32
37
  "@cloudflare/workers-types": "^5.20260729.1",
33
38
  "@hono/zod-validator": "^0.9.0",
34
- "@pithy-sh/core": "^0.1.2",
35
- "@pithy-sh/email": "^0.1.2",
36
- "@pithy-sh/secrets": "^0.1.2",
37
- "@pithy-sh/storage": "^0.1.2",
38
- "hono": "^4.13.2",
39
- "kysely": "^0.29.0",
39
+ "@pithy-sh/core": "^0.1.3",
40
+ "@pithy-sh/email": "^0.1.3",
41
+ "@pithy-sh/secrets": "^0.1.3",
42
+ "@pithy-sh/storage": "^0.1.3",
40
43
  "postal-mime": "^3.0.0",
41
- "tldts": "^7.4.10",
42
- "zod": "^4.0.0"
44
+ "tldts": "^7.4.10"
43
45
  },
44
46
  "peerDependencies": {
45
- "@pithy-sh/auth": "^0.1.2",
46
- "@pithy-sh/payments": "^0.1.2"
47
+ "@pithy-sh/auth": "^0.1.3",
48
+ "@pithy-sh/payments": "^0.1.3",
49
+ "hono": "^4.13.2",
50
+ "kysely": "^0.29.0",
51
+ "zod": "^4.4.0"
47
52
  },
48
53
  "peerDependenciesMeta": {
49
54
  "@pithy-sh/auth": {
@@ -60,9 +65,13 @@
60
65
  "@pithy-sh/tsconfig": "workspace:*",
61
66
  "@types/node": "^22.15.0",
62
67
  "@vitest/coverage-v8": "^4.1.0",
68
+ "hono": "^4.13.2",
69
+ "kysely": "^0.29.0",
63
70
  "kysely-d1": "^0.4.0",
71
+ "tsdown": "^0.23.0",
64
72
  "typescript": "^7.0.2",
65
73
  "vitest": "^4.1.0",
66
- "wrangler": "^4.115.0"
74
+ "wrangler": "^4.115.0",
75
+ "zod": "^4.4.0"
67
76
  }
68
77
  }
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/support";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.1.2";
16
+ export const PACKAGE_VERSION = "0.1.3";