@pithy-sh/support 0.3.3 → 0.4.0

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 (42) hide show
  1. package/dist/ai/classify.d.ts.map +1 -1
  2. package/dist/ai/classify.js +3 -3
  3. package/dist/http/handlers.d.ts.map +1 -1
  4. package/dist/http/handlers.js +2 -3
  5. package/dist/http/routes.d.ts.map +1 -1
  6. package/dist/http/routes.js +60 -12
  7. package/dist/inbound/handler.d.ts.map +1 -1
  8. package/dist/inbound/handler.js +0 -1
  9. package/dist/inbound/ingest.d.ts +0 -2
  10. package/dist/inbound/ingest.d.ts.map +1 -1
  11. package/dist/inbound/ingest.js +0 -14
  12. package/dist/provision/provisionSupport.d.ts +12 -3
  13. package/dist/provision/provisionSupport.d.ts.map +1 -1
  14. package/dist/reply/send.d.ts +0 -2
  15. package/dist/reply/send.d.ts.map +1 -1
  16. package/dist/reply/send.js +2 -15
  17. package/dist/store/search.d.ts +16 -9
  18. package/dist/store/search.d.ts.map +1 -1
  19. package/dist/store/search.js +16 -9
  20. package/dist/store/searchIndex.d.ts +131 -5
  21. package/dist/store/searchIndex.d.ts.map +1 -1
  22. package/dist/store/searchIndex.js +151 -6
  23. package/dist/store/threads.d.ts.map +1 -1
  24. package/dist/store/threads.js +3 -2
  25. package/dist/submission/submit.d.ts +0 -2
  26. package/dist/submission/submit.d.ts.map +1 -1
  27. package/dist/submission/submit.js +0 -14
  28. package/dist/version.generated.d.ts +1 -1
  29. package/dist/version.generated.js +1 -1
  30. package/package.json +9 -9
  31. package/src/ai/classify.ts +7 -3
  32. package/src/http/handlers.ts +2 -3
  33. package/src/http/routes.ts +92 -18
  34. package/src/inbound/handler.ts +0 -1
  35. package/src/inbound/ingest.ts +0 -14
  36. package/src/provision/provisionSupport.ts +12 -5
  37. package/src/reply/send.ts +2 -13
  38. package/src/store/search.ts +16 -9
  39. package/src/store/searchIndex.ts +176 -5
  40. package/src/store/threads.ts +2 -1
  41. package/src/submission/submit.ts +0 -11
  42. package/src/version.generated.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/ai/classify.ts"],"names":[],"mappings":"AAKA,OAAO,EAAoC,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAiB,MAAM,eAAe,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0GAA0G;AAC1G,MAAM,WAAW,SAAS;IACxB,qGAAqG;IACrG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,QAAQ,EAAE,eAAe,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf;AA4BD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,iBAAiB,GAAG,MAAM,CAe1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS3F;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,SAAS,EACb,OAAO,EAAE;IACP,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,cAAc,CAAC,CAkEzB"}
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/ai/classify.ts"],"names":[],"mappings":"AAKA,OAAO,EAAoC,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAiB,MAAM,eAAe,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0GAA0G;AAC1G,MAAM,WAAW,SAAS;IACxB,qGAAqG;IACrG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,QAAQ,EAAE,eAAe,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf;AA4BD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,iBAAiB,GAAG,MAAM,CAe1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS3F;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,SAAS,EACb,OAAO,EAAE;IACP,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,cAAc,CAAC,CAsEzB"}
@@ -108,11 +108,11 @@ function unsure(model) {
108
108
  * The code is the whole of how the step can tell the two apart.
109
109
  */
110
110
  async function classifyMessage(ai, options) {
111
- const run = ai?.run;
112
- if (typeof run !== "function") throw new SupportClassificationError({ detail: "the AI binding is absent from this worker's env; classification cannot run" });
111
+ const binding = ai;
112
+ if (typeof binding?.run !== "function") throw new SupportClassificationError({ detail: "the AI binding is absent from this worker's env; classification cannot run" });
113
113
  let raw;
114
114
  try {
115
- raw = await run.call(ai, options.model, {
115
+ raw = await binding.run(options.model, {
116
116
  messages: [{
117
117
  role: "system",
118
118
  content: classificationPrompt(options.categories)
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAM9D,OAAO,KAAK,EAEV,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,2BAA2B,EAC3B,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAGnB;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,wFAAwF;IACxF,EAAE,EAAE,UAAU,CAAC;IACf,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,0CAA0C;IAC1C,GAAG,EAAE,OAAO,CAAC;IACb,gDAAgD;IAChD,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACrD,8FAA8F;IAC9F,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,8GAA8G;IAC9G,KAAK,EAAE,WAAW,CAAC;IACnB,sBAAsB;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,sBAAsB;AACtB,wBAAsB,SAAS,CAC7B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,sBAAsB,CAAC,CAkBjC;AAqCD,qCAAqC;AACrC,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0B1G;AAED,8CAA8C;AAC9C,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,CAAC,CAgB5B;AAED,2BAA2B;AAC3B,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,wBAAwB,CAAC,CAkBnC;AAED,0EAA0E;AAC1E,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,2BAA2B,CAAC,CAkCtC;AAED,sCAAsC;AACtC,wBAAsB,WAAW,CAC/B,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,oBAAoB,CAAC,CAa/B;AAED,gCAAgC;AAChC,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAEtF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,mBAAmB,EAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,yBAAyB,CAAC,CAuEpC;AAED,kDAAkD;AAClD,wBAAsB,aAAa,CACjC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,wBAAwB,CAAC,CAGnC;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,uBAAuB,CAAC,CAgBlC"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ import { submitFeedback } from "../submission/submit.js";
12
12
  import { latestInboundMessageId } from "../workflows/classify.js";
13
13
  import { listedThreadView, messageView, myMessageView, myThreadView, senderView, threadView } from "./views.js";
14
14
  import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
15
+ import { pithyCommandOf } from "@pithy-sh/core/src/cli/command";
15
16
  //#region src/http/handlers.ts
16
17
  /** List the inbox. */
17
18
  async function listInbox(deps, query, viewer) {
@@ -98,7 +99,6 @@ async function replyToConversation(deps, threadId, input, viewer) {
98
99
  db: deps.db,
99
100
  config: deps.config,
100
101
  enqueue: deps.enqueue,
101
- fts: deps.fts,
102
102
  emit: deps.emit,
103
103
  log: deps.log,
104
104
  newId: deps.newId,
@@ -119,7 +119,7 @@ async function reclassifyConversation(deps, threadId, viewer) {
119
119
  });
120
120
  if (!await deps.dispatchClassify(messageId)) throw new SupportClassificationError({
121
121
  message: "Classification is not available on this deployment.",
122
- action: "Run `pithy support provision` to deploy the classification worker, then retry.",
122
+ action: `Run \`${pithyCommandOf("support provision", {}, { runtime: true })}\` to deploy the classification worker, then retry.`,
123
123
  detail: `the SUPPORT_CLASSIFY workflow binding is absent, so no instance was started`
124
124
  });
125
125
  await deps.emit({
@@ -184,7 +184,6 @@ async function submitFeedbackRequest(deps, input, userId) {
184
184
  config: deps.config,
185
185
  categories: deps.categories,
186
186
  bucket: deps.bucket,
187
- fts: deps.fts,
188
187
  resolveAccount: (id) => resolveSubmitterAccount(deps.d1, id, deps.peers),
189
188
  dispatchClassify: deps.dispatchClassify,
190
189
  emit: deps.emit,
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAK7E,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EAEL,KAAK,WAAW,EAUjB,MAAM,YAAY,CAAC;AAoBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CACjE;AAuCD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,CAqFtG"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAM7E,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EAEL,KAAK,WAAW,EAUjB,MAAM,YAAY,CAAC;AA+BpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CACjE;AAuCD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,CAoJtG"}
@@ -2,13 +2,14 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { requireAuth } from "./guards.js";
4
4
  import { archiveConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, submitFeedbackRequest, updateFlags } from "./handlers.js";
5
+ import { SupportArchiveResponse, SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportRepliesResponse, SupportReplySentResponse, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse } from "./responses.js";
5
6
  import { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput, ThreadIdParam } from "./schemas.js";
6
7
  import { SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE } from "./scopes.js";
7
8
  import { InternalError } from "@pithy-sh/core/src/error/pithyError";
8
- import { zValidator } from "@hono/zod-validator";
9
9
  import { requireControlPlane } from "@pithy-sh/core/src/controlPlane/http/guard";
10
+ import { describeRoute } from "@pithy-sh/core/src/http/routeSpec";
10
11
  import { requireSameOrigin } from "@pithy-sh/core/src/http/sameOrigin";
11
- import { validationHook } from "@pithy-sh/core/src/http/validation";
12
+ import { validate } from "@pithy-sh/core/src/http/validation";
12
13
  //#region src/http/routes.ts
13
14
  /**
14
15
  * The verified management client behind a control-plane call.
@@ -47,17 +48,64 @@ function registerSupportRoutes(options) {
47
48
  const resolve = options.resolveDeps;
48
49
  const submission = options.submission ?? true;
49
50
  return (app) => {
50
- app.get(`${base}/threads`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("query", ListThreadsQuery, validationHook), async (c) => c.json(await listInbox(await resolve(c), c.req.valid("query"), viewer(c))));
51
- app.get(`${base}/replies`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("query", RepliesQuery, validationHook), async (c) => c.json({ replies: listReplies(await resolve(c), c.req.valid("query")) }));
52
- app.get(`${base}/threads/:id`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await readConversation(await resolve(c), c.req.valid("param").id)));
53
- app.post(`${base}/threads/:id/archive`, requireControlPlane(SUPPORT_THREADS_ARCHIVE_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", ArchiveThreadInput, validationHook), async (c) => c.json(await archiveConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
54
- app.post(`${base}/threads/:id/reply`, requireControlPlane(SUPPORT_THREADS_REPLY_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", ReplyInput, validationHook), async (c) => c.json(await replyToConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c)), 201));
55
- app.post(`${base}/threads/:id/reclassify`, requireControlPlane(SUPPORT_THREADS_RECLASSIFY_SCOPE), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await reclassifyConversation(await resolve(c), c.req.valid("param").id, viewer(c))));
56
- app.post(`${base}/threads/:id/flags`, requireControlPlane(SUPPORT_THREADS_FLAG_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", FlagsInput, validationHook), async (c) => c.json(await updateFlags(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
51
+ app.get(`${base}/threads`, describeRoute({
52
+ summary: "List the support inbox.",
53
+ verification: "control-plane",
54
+ scopes: [SUPPORT_THREADS_READ_SCOPE],
55
+ responses: { 200: SupportThreadsResponse }
56
+ }), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("query", ListThreadsQuery), async (c) => c.json(await listInbox(await resolve(c), c.req.valid("query"), viewer(c))));
57
+ app.get(`${base}/replies`, describeRoute({
58
+ summary: "List the canned replies.",
59
+ verification: "control-plane",
60
+ scopes: [SUPPORT_THREADS_READ_SCOPE],
61
+ responses: { 200: SupportRepliesResponse }
62
+ }), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("query", RepliesQuery), async (c) => c.json({ replies: listReplies(await resolve(c), c.req.valid("query")) }));
63
+ app.get(`${base}/threads/:id`, describeRoute({
64
+ summary: "Read one conversation and its messages.",
65
+ verification: "control-plane",
66
+ scopes: [SUPPORT_THREADS_READ_SCOPE],
67
+ responses: { 200: SupportThreadResponse }
68
+ }), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("param", ThreadIdParam), async (c) => c.json(await readConversation(await resolve(c), c.req.valid("param").id)));
69
+ app.post(`${base}/threads/:id/archive`, describeRoute({
70
+ summary: "Mark a conversation done, or reopen it.",
71
+ verification: "control-plane",
72
+ scopes: [SUPPORT_THREADS_ARCHIVE_SCOPE],
73
+ responses: { 200: SupportArchiveResponse }
74
+ }), requireControlPlane(SUPPORT_THREADS_ARCHIVE_SCOPE), validate("param", ThreadIdParam), validate("json", ArchiveThreadInput), async (c) => c.json(await archiveConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
75
+ app.post(`${base}/threads/:id/reply`, describeRoute({
76
+ summary: "Answer the customer.",
77
+ verification: "control-plane",
78
+ scopes: [SUPPORT_THREADS_REPLY_SCOPE],
79
+ responses: { 201: SupportReplySentResponse }
80
+ }), requireControlPlane(SUPPORT_THREADS_REPLY_SCOPE), validate("param", ThreadIdParam), validate("json", ReplyInput), async (c) => c.json(await replyToConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c)), 201));
81
+ app.post(`${base}/threads/:id/reclassify`, describeRoute({
82
+ summary: "Run the classifier again on the latest inbound message.",
83
+ verification: "control-plane",
84
+ scopes: [SUPPORT_THREADS_RECLASSIFY_SCOPE],
85
+ responses: { 200: SupportReclassifiedResponse }
86
+ }), requireControlPlane(SUPPORT_THREADS_RECLASSIFY_SCOPE), validate("param", ThreadIdParam), async (c) => c.json(await reclassifyConversation(await resolve(c), c.req.valid("param").id, viewer(c))));
87
+ app.post(`${base}/threads/:id/flags`, describeRoute({
88
+ summary: "Set the caller's private read and snooze flags.",
89
+ verification: "control-plane",
90
+ scopes: [SUPPORT_THREADS_FLAG_SCOPE],
91
+ responses: { 200: SupportFlagsResponse }
92
+ }), requireControlPlane(SUPPORT_THREADS_FLAG_SCOPE), validate("param", ThreadIdParam), validate("json", FlagsInput), async (c) => c.json(await updateFlags(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
57
93
  if (!submission) return;
58
- app.post(`${base}/feedback`, requireAuth(), requireSameOrigin(), zValidator("json", SubmitFeedbackInput, validationHook), async (c) => c.json(await submitFeedbackRequest(await resolve(c), c.req.valid("json"), submitter(c)), 201));
59
- app.get(`${base}/feedback`, requireAuth(), zValidator("query", MyThreadsQuery, validationHook), async (c) => c.json(await listMyThreads(await resolve(c), c.req.valid("query"), submitter(c))));
60
- app.get(`${base}/feedback/:id`, requireAuth(), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await readMyThread(await resolve(c), c.req.valid("param").id, submitter(c))));
94
+ app.post(`${base}/feedback`, describeRoute({
95
+ summary: "Write in to support from the app.",
96
+ verification: ["bearer", "session"],
97
+ responses: { 201: SupportSubmissionResponse }
98
+ }), requireAuth(), requireSameOrigin(), validate("json", SubmitFeedbackInput), async (c) => c.json(await submitFeedbackRequest(await resolve(c), c.req.valid("json"), submitter(c)), 201));
99
+ app.get(`${base}/feedback`, describeRoute({
100
+ summary: "List the caller's own support requests.",
101
+ verification: ["bearer", "session"],
102
+ responses: { 200: SupportMyThreadsResponse }
103
+ }), requireAuth(), validate("query", MyThreadsQuery), async (c) => c.json(await listMyThreads(await resolve(c), c.req.valid("query"), submitter(c))));
104
+ app.get(`${base}/feedback/:id`, describeRoute({
105
+ summary: "Read one of the caller's own support requests.",
106
+ verification: ["bearer", "session"],
107
+ responses: { 200: SupportMyThreadResponse }
108
+ }), requireAuth(), validate("param", ThreadIdParam), async (c) => c.json(await readMyThread(await resolve(c), c.req.valid("param").id, submitter(c))));
61
109
  };
62
110
  }
63
111
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/inbound/handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAEvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAMnD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,sBAAsB,CAkEvF"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/inbound/handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAEvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAMnD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,sBAAsB,CAiEvF"}
@@ -57,7 +57,6 @@ function createSupportEmailHandler(wiring) {
57
57
  db: supportDatabase(d1),
58
58
  config: wiring.config,
59
59
  bucket: bindings.SUPPORT_BUCKET,
60
- fts: wiring.config.search.fts,
61
60
  dispatchClassify: makeClassifyDispatcher(env, log),
62
61
  linkSender: (address) => resolveSenderUserId(d1, address, wiring.peers),
63
62
  emit: wiring.emit,
@@ -72,8 +72,6 @@ export interface IngestDeps {
72
72
  config: SupportConfig;
73
73
  /** The R2 bucket raw messages and attachments are written to. Absent means neither is kept. */
74
74
  bucket?: R2Bucket;
75
- /** Whether the FTS5 index is composed. False means search runs as a `LIKE` scan and nothing is indexed. */
76
- fts: boolean;
77
75
  /**
78
76
  * Start the classification Workflow for a stored message. Allowed to fail and allowed to decline —
79
77
  * an unprovisioned project has no binding, and a thread that stays `uncategorized` is a legitimate
@@ -1 +1 @@
1
- {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/inbound/ingest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAG/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAWxB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uBAAuB;AACvB,MAAM,MAAM,aAAa;AACvB,oFAAoF;AAClF;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE;AAC7C,wDAAwD;GACtD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAC3D,yEAAyE;GACvE;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACzE,cAAc;GACZ;IACE,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,KAAK,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,iGAAiG;IACjG,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEN,+CAA+C;AAC/C,MAAM,WAAW,UAAU;IACzB,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,2GAA2G;IAC3G,GAAG,EAAE,OAAO,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,oFAAoF;IACpF,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,sBAAsB;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,GAAG,EAAE,WAAW,CAAC;IACjB,wGAAwG;IACxG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0ID,kCAAkC;AAClC,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAsShG"}
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/inbound/ingest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAG/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAUxB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uBAAuB;AACvB,MAAM,MAAM,aAAa;AACvB,oFAAoF;AAClF;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE;AAC7C,wDAAwD;GACtD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAC3D,yEAAyE;GACvE;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACzE,cAAc;GACZ;IACE,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,KAAK,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,iGAAiG;IACjG,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEN,+CAA+C;AAC/C,MAAM,WAAW,UAAU;IACzB,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,oFAAoF;IACpF,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,sBAAsB;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,GAAG,EAAE,WAAW,CAAC;IACjB,wGAAwG;IACxG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0ID,kCAAkC;AAClC,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CA2RhG"}
@@ -7,7 +7,6 @@ import { SUPPORT_ATTACHMENTS_TABLE, SUPPORT_MESSAGES_TABLE, SUPPORT_THREADS_TABL
7
7
  import { attachmentKey, putAttachment, rawMessageKey, sha256Hex } from "../attachment/store.js";
8
8
  import { SupportAuditActions } from "../audit/actions.js";
9
9
  import { parentCandidates } from "../mime/threading.js";
10
- import { indexMessage } from "../store/search.js";
11
10
  import { truncateToBytes } from "../mime/truncate.js";
12
11
  import { MAX_TEXT_BODY, parseInbound } from "../mime/parse.js";
13
12
  import { checkRates, checkSize } from "./guard.js";
@@ -283,19 +282,6 @@ async function ingestInbound(deps, input) {
283
282
  });
284
283
  throw error;
285
284
  }
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
285
  let attachments = 0;
300
286
  try {
301
287
  attachments = await storeAttachments(deps, parsed, threadId, messageId, now);
@@ -69,17 +69,25 @@ export interface SupportProvisioner {
69
69
  /** Deploy the prebuilt classification worker for this environment. */
70
70
  deployWorker(env: ManagedEnvironment): Promise<void>;
71
71
  /**
72
- * Create or drop the full-text index in this environment's app database, to match `search.fts`.
72
+ * Create or drop the full-text index **and its triggers** in this environment's app database, to
73
+ * match `search.fts`.
73
74
  *
74
75
  * A provisioning step rather than a migration, because the index is **derived** — every row in it
75
76
  * comes from `pithy_support_messages` and `reindexThread` rebuilds it on demand. That is the line a
76
77
  * migration is for: schema whose loss loses data. Keeping it here is also what makes the flag safe
77
78
  * to toggle at all, since a config-conditional migration removed from the set is corruption to
78
- * Kysely and blocks `pithy migrate` for every capability sharing the database.
79
+ * Kysely and blocks `pithy migrate` for every capability sharing the database. The triggers inherit
80
+ * that constraint exactly: they exist only where the table does, so they are provisioning too.
81
+ *
82
+ * **Three outcomes, not two.** `repaired` is a database whose table was already there and whose
83
+ * triggers were not — an adopter who took the release that moved the index onto triggers and deployed
84
+ * without re-provisioning. Nothing in the kit maintains that index, so the run creates the triggers
85
+ * and backfills the gap, and says which of the three it did.
79
86
  */
80
87
  ensureSearchIndex(env: ManagedEnvironment): Promise<{
81
88
  created: boolean;
82
89
  dropped: boolean;
90
+ repaired: boolean;
83
91
  }>;
84
92
  /**
85
93
  * Ensure the inbound Email Routing rule that delivers the support address to the app worker.
@@ -100,11 +108,12 @@ export interface SupportProvisionResult {
100
108
  };
101
109
  /** The environments a classification worker was deployed for. */
102
110
  environments: ManagedEnvironment[];
103
- /** What the full-text index did, per environment — created, dropped, or already correct. */
111
+ /** What the full-text index did, per environment — created, dropped, repaired, or already correct. */
104
112
  search: Array<{
105
113
  env: ManagedEnvironment;
106
114
  created: boolean;
107
115
  dropped: boolean;
116
+ repaired: boolean;
108
117
  }>;
109
118
  /** Whether the inbound routing rule was created, already present, or skipped. */
110
119
  routing: {
@@ -1 +1 @@
1
- {"version":3,"file":"provisionSupport.d.ts","sourceRoot":"","sources":["../../src/provision/provisionSupport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provisionSupport.d.ts","sourceRoot":"","sources":["../../src/provision/provisionSupport.ts"],"names":[],"mappings":"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"}
@@ -78,8 +78,6 @@ export interface ReplyDeps {
78
78
  }) => Promise<{
79
79
  jobId: string;
80
80
  }>;
81
- /** Whether the full-text index is in use. */
82
- fts: boolean;
83
81
  /** The audit seam. */
84
82
  emit: AuditEmit;
85
83
  /** The request logger. */
@@ -1 +1 @@
1
- {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/reply/send.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAiD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,+FAA+F;QAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjC,6CAA6C;IAC7C,GAAG,EAAE,OAAO,CAAC;IACb,sBAAsB;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,0FAA0F;IAC1F,OAAO,EAAE,KAAK,CAAC;IACf,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEN,gCAAgC;AAChC,wBAAsB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CA2LxF"}
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/reply/send.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAiD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,0BAA0B;IAC1B,EAAE,EAAE,eAAe,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,+FAA+F;QAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjC,sBAAsB;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,0FAA0F;IAC1F,OAAO,EAAE,KAAK,CAAC;IACf,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEN,gCAAgC;AAChC,wBAAsB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAkLxF"}
@@ -5,7 +5,7 @@ import { SupportMessage } from "../data/message.js";
5
5
  import { SUPPORT_MESSAGES_TABLE, SUPPORT_THREADS_TABLE } from "../data/tables.js";
6
6
  import { SupportAuditActions } from "../audit/actions.js";
7
7
  import { buildReferencesHeader, replySubject } from "../mime/threading.js";
8
- import { indexMessage } from "../store/search.js";
8
+ import { commandName } from "@pithy-sh/core/src/cli/command";
9
9
  //#region src/reply/send.ts
10
10
  /** Send a reply on a thread. */
11
11
  async function sendReply(deps, input) {
@@ -60,7 +60,7 @@ async function sendReply(deps, input) {
60
60
  if (!inApp) {
61
61
  if (!enqueue) throw new SupportReplyFailedError({
62
62
  message: "This deployment cannot send mail.",
63
- action: "Add the email capability (`pithy add email`) and provision it, then retry.",
63
+ action: `Compose the email capability into this Worker with \`${commandName("add")}\` and provision it, then retry.`,
64
64
  detail: `thread ${input.threadId} needs a mailed reply and no email capability is composed`
65
65
  });
66
66
  if (!replyTo) throw new SupportReplyFailedError({
@@ -110,19 +110,6 @@ async function sendReply(deps, input) {
110
110
  createdAt: now
111
111
  };
112
112
  await deps.db.insertInto(SUPPORT_MESSAGES_TABLE).values(SupportMessage.encode(message)).execute();
113
- if (deps.fts) try {
114
- await indexMessage(deps.db, {
115
- threadId: input.threadId,
116
- messageId,
117
- subject,
118
- body: input.body
119
- });
120
- } catch (error) {
121
- deps.log.warn("support reply not indexed", {
122
- messageId,
123
- error
124
- });
125
- }
126
113
  await deps.db.updateTable(SUPPORT_THREADS_TABLE).set((eb) => ({
127
114
  messageCount: eb("messageCount", "+", 1),
128
115
  lastMessageAt: now.getTime(),
@@ -26,16 +26,23 @@ export declare function searchPredicate(term: string, options: {
26
26
  /**
27
27
  * Add a message to the full-text index, replacing any row already there for it.
28
28
  *
29
- * **Remove-then-insert, not a bare insert** — the same shape the CMS `syncFts` uses, and for the
30
- * reason application-managed indexes always need it: an FTS5 table has no primary key and no unique
31
- * constraint to lean on, so a second call for the same message would silently add a second copy and
32
- * the thread would start appearing twice in its own search results. Making the write idempotent means
33
- * a retry, a reindex, and a first index are all the same operation, which is the property every other
34
- * write path in this repo is built on.
29
+ * **No write path calls this any more.** The index is maintained by the triggers `createSearchIndex`
30
+ * provisions (`store/searchIndex.ts`), so a message is indexed because it was written rather than
31
+ * because the code that wrote it remembered. This is the **repair** primitive now: {@link reindexThread}
32
+ * and {@link reindexAll} are its only callers in the kit, and it stays exported because
33
+ * `package.json` exports `./src/*`, so an adopter may be reaching it.
35
34
  *
36
- * Best-effort by contract: a failed index write must never lose a customer's message, so the caller
37
- * logs and carries on — the row is still there, still readable, still repairable by
38
- * {@link reindexThread}, and only missing from one search box.
35
+ * **Remove-then-insert, not a bare insert** — the same shape the trigger bodies use, and for the
36
+ * reason an FTS5 table always needs it: it has no primary key and no unique constraint to lean on, so
37
+ * a second call for the same message would silently add a second copy and the thread would start
38
+ * appearing twice in its own search results. Making the write idempotent means a retry, a reindex, and
39
+ * a first index are all the same operation.
40
+ *
41
+ * **It no longer carries a swallow contract, because it is no longer on a message write.** A failure
42
+ * here is a failed repair, and the caller is a command or a route that can say so. The durability
43
+ * question moved with the index itself: a trigger runs inside the message's own statement, so a
44
+ * failure in its body takes the message write down with it — which is what `searchIndex.ts` documents
45
+ * and what the triggers' own tests hold.
39
46
  */
40
47
  export declare function indexMessage(db: SupportDatabase, entry: {
41
48
  threadId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/store/search.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAO,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA+B9E;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAkB5F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,eAAe,EACnB,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5E,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,CAAC,CA4CjB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/store/search.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAO,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA+B9E;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAkB5F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,eAAe,EACnB,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5E,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,eAAe,EACnB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,CAAC,CA4CjB"}
@@ -67,16 +67,23 @@ function searchPredicate(term, options) {
67
67
  /**
68
68
  * Add a message to the full-text index, replacing any row already there for it.
69
69
  *
70
- * **Remove-then-insert, not a bare insert** — the same shape the CMS `syncFts` uses, and for the
71
- * reason application-managed indexes always need it: an FTS5 table has no primary key and no unique
72
- * constraint to lean on, so a second call for the same message would silently add a second copy and
73
- * the thread would start appearing twice in its own search results. Making the write idempotent means
74
- * a retry, a reindex, and a first index are all the same operation, which is the property every other
75
- * write path in this repo is built on.
70
+ * **No write path calls this any more.** The index is maintained by the triggers `createSearchIndex`
71
+ * provisions (`store/searchIndex.ts`), so a message is indexed because it was written rather than
72
+ * because the code that wrote it remembered. This is the **repair** primitive now: {@link reindexThread}
73
+ * and {@link reindexAll} are its only callers in the kit, and it stays exported because
74
+ * `package.json` exports `./src/*`, so an adopter may be reaching it.
76
75
  *
77
- * Best-effort by contract: a failed index write must never lose a customer's message, so the caller
78
- * logs and carries on — the row is still there, still readable, still repairable by
79
- * {@link reindexThread}, and only missing from one search box.
76
+ * **Remove-then-insert, not a bare insert** — the same shape the trigger bodies use, and for the
77
+ * reason an FTS5 table always needs it: it has no primary key and no unique constraint to lean on, so
78
+ * a second call for the same message would silently add a second copy and the thread would start
79
+ * appearing twice in its own search results. Making the write idempotent means a retry, a reindex, and
80
+ * a first index are all the same operation.
81
+ *
82
+ * **It no longer carries a swallow contract, because it is no longer on a message write.** A failure
83
+ * here is a failed repair, and the caller is a command or a route that can say so. The durability
84
+ * question moved with the index itself: a trigger runs inside the message's own statement, so a
85
+ * failure in its body takes the message write down with it — which is what `searchIndex.ts` documents
86
+ * and what the triggers' own tests hold.
80
87
  */
81
88
  async function indexMessage(db, entry) {
82
89
  await sql`delete from pithy_support_search where message_id = ${entry.messageId}`.execute(db);
@@ -3,8 +3,8 @@
3
3
  import { type Kysely } from "kysely";
4
4
  import type { SupportDatabase } from "../data/tables";
5
5
  /**
6
- * The FTS5 index's lifecycle — created and dropped by `pithy support provision`, **not by a
7
- * migration**.
6
+ * The FTS5 index's lifecycle — the virtual table **and the three triggers that keep it in step**,
7
+ * created and dropped by `pithy support provision`, **not by a migration**.
8
8
  *
9
9
  * ## Why this is not a migration
10
10
  *
@@ -26,13 +26,128 @@ import type { SupportDatabase } from "../data/tables";
26
26
  * config flag, fixable only by knowing to roll back first. Out of the ledger, there is nothing to
27
27
  * corrupt and toggling either way is just a re-provision.
28
28
  *
29
- * Both statements are idempotent, so provisioning stays safe to re-run — the property every other
29
+ * **The triggers inherit that constraint exactly.** They exist only where the table exists, so they
30
+ * are provisioning and not schema — a `CREATE TRIGGER` composed conditionally on `search.fts` would
31
+ * corrupt the ledger for exactly the same reason the table's own statement did. `migrations.workers.test.ts`
32
+ * keeps proving the migration set carries none.
33
+ *
34
+ * ## Why triggers at all
35
+ *
36
+ * The index used to be written by hand at every message write — three call sites, each best-effort —
37
+ * and correctness depended on every future author remembering. There is no shared message-write
38
+ * helper, so a fourth path that forgot produced a thread nobody could search, with nothing failing,
39
+ * nothing logging and no test catching it.
40
+ *
41
+ * `CREATE TRIGGER` was in an undocumented middle on D1 when that was decided: D1 runs an authorizer
42
+ * that rejects SQLite constructs workerd permits (`fts5vocab` is refused where plain FTS5 succeeds),
43
+ * so a trigger passing under Miniflare proved nothing about the deployment that matters. That is
44
+ * settled — the Leed CMS ships a `BEFORE UPDATE … BEGIN SELECT RAISE(ABORT, …); END` trigger in a
45
+ * production migration, so the authorizer permits both the statement and a body that aborts.
46
+ *
47
+ * **The durability contract inverts, deliberately.** A trigger runs inside the message's own
48
+ * statement, so a failure in its body takes the message write down with it — where the old call sites
49
+ * logged and carried on. That swallow existed because a second round trip to a separate table can
50
+ * fail on its own; inside one statement it cannot. The body is a `DELETE` plus an `INSERT` of values
51
+ * already present in `NEW.*`, with no conversion, no constraint and no foreign key, so what remains is
52
+ * the FTS table being absent — which is what {@link SEARCH_TRIGGERS} cannot outlive, since
53
+ * {@link dropSearchIndex} drops the pair together — or disk and corruption, which would have failed the
54
+ * message insert anyway.
55
+ *
56
+ * The one gap the kit cannot close from here is an adopter who upgrades and deploys without re-running
57
+ * `pithy support provision`: the calls are gone, the triggers were never created, and messages
58
+ * silently stop being indexed. `pithy doctor` reports a provisioned table with no triggers as drift
59
+ * and names that command, per environment.
60
+ *
61
+ * Every statement here is idempotent, so provisioning stays safe to re-run — the property every other
30
62
  * step of `pithy support provision` already has.
31
63
  */
32
64
  /** The virtual table's name. Snake_case because nothing translates it — every statement here is hand-written. */
33
65
  export declare const SEARCH_TABLE = "pithy_support_search";
34
66
  /**
35
- * Create the full-text index if it is not already there.
67
+ * The three triggers, in create order — `_ai`, `_au`, `_ad` after SQLite's own FTS5 naming for the
68
+ * insert, update and delete hooks.
69
+ *
70
+ * Exported because two other places need the exact names and may not re-derive them: `pithy doctor`
71
+ * asks a deployed database whether they are there, and {@link dropSearchIndex} drops them. They carry
72
+ * the `pithy_support_` prefix every table in this capability does, for the same reason — `sqlite_master`
73
+ * is one flat namespace per database and the prefix is the only partition.
74
+ */
75
+ export declare const SEARCH_TRIGGERS: readonly ["pithy_support_search_ai", "pithy_support_search_au", "pithy_support_search_ad"];
76
+ /**
77
+ * Every name a provisioner or `pithy doctor` asks `sqlite_master` about — the table and its triggers.
78
+ *
79
+ * One list, read by both, because the two have to agree about what "provisioned" means. A doctor that
80
+ * looked for two triggers while the provisioner created three would report drift nothing could clear.
81
+ */
82
+ export declare const SEARCH_OBJECTS: readonly string[];
83
+ /** What is actually in a database: the virtual table, and how much of the trigger set. */
84
+ export interface SearchIndexState {
85
+ /** Whether `pithy_support_search` exists. */
86
+ table: boolean;
87
+ /** How many of the three triggers exist — `"some"` is drift, not a pass. */
88
+ triggers: "all" | "some" | "none";
89
+ }
90
+ /**
91
+ * Read a `sqlite_master` name listing into the two facts anything acts on.
92
+ *
93
+ * **The trigger count is three-valued, and `"some"` is why.** A partial set means one `CREATE TRIGGER`
94
+ * failed or one was dropped by hand, and the consequence is per statement kind — two of three leaves
95
+ * updates or deletes going unindexed while inserts look fine, which is the worst way round. Folding it
96
+ * into `"none"` would also hide it from a teardown: `search.fts: false` has to clear a stray trigger,
97
+ * because a trigger whose table is gone fails every write to `pithy_support_messages`.
98
+ */
99
+ export declare function searchIndexState(names: readonly string[]): SearchIndexState;
100
+ /** What a provision run has to do to one environment's index. */
101
+ export type SearchIndexAction =
102
+ /** Already in the state `search.fts` asks for. */
103
+ "none"
104
+ /** The table is not there: create it, its triggers, and backfill. */
105
+ | "create"
106
+ /** The table is there and its triggers are not: create them, and backfill the gap they left. */
107
+ | "repair"
108
+ /** `search.fts` is off and something is there: drop the triggers and the table. */
109
+ | "drop";
110
+ /**
111
+ * Decide what `pithy support provision` does to one environment's index.
112
+ *
113
+ * **`repair` is the member this release exists for.** A run used to compare one boolean against one
114
+ * boolean — table present against flag set — and return early when they matched. That is exactly the
115
+ * state an adopter is in after upgrading and deploying: the table is present, the flag is set, the
116
+ * `indexMessage` calls are gone from every write path, and the triggers were never created. The old
117
+ * comparison read that as nothing to do, so the command `pithy doctor` names would not have been the
118
+ * command that fixes it.
119
+ *
120
+ * A repair backfills as a create does, because the gap is real: messages written between the deploy
121
+ * and the re-provision had no trigger to fire for them and no application call either.
122
+ *
123
+ * `drop` fires on triggers alone as well as on the table. A trigger body resolves its table at run
124
+ * time, so a trigger outlives a hand-dropped table — and while it does, every write to
125
+ * `pithy_support_messages` fails. Turning the flag off has to clear that rather than read it as a
126
+ * database with no index.
127
+ */
128
+ export declare function searchIndexAction(state: SearchIndexState, wanted: boolean): SearchIndexAction;
129
+ /**
130
+ * The `CREATE TRIGGER` statements, one per {@link SEARCH_TRIGGERS} entry and in that order.
131
+ *
132
+ * **`BEGIN` and `END` are uppercase, and that is load-bearing.** Miniflare accepts a lowercase
133
+ * compound-statement body; remote D1 rejects it as `incomplete input [code: 7500]`, so a lowercased
134
+ * body passes every local test in this repository and fails on deploy. `searchIndex.test.ts` fails on
135
+ * the lowercase form, because no Workers test can.
136
+ *
137
+ * **Every column is the physical snake_case name.** `CamelCasePlugin` snake-cases the identifiers the
138
+ * query builder emits and never touches a string inside a `sql` template, so this is the one place in
139
+ * the package that has to spell them itself: `NEW.id`, `NEW.thread_id`, `NEW.subject`, `NEW.text_body`
140
+ * — never the camelCase keys `data/tables.ts` declares.
141
+ *
142
+ * **Insert and update both delete first.** An FTS5 table has no primary key and no unique constraint
143
+ * to lean on, so a bare insert on a second write for the same message would add a second copy and the
144
+ * thread would start appearing twice in its own search results — the same reason `indexMessage` is
145
+ * remove-then-insert. The update body keys its delete on `OLD.id`, so a row whose id changed takes its
146
+ * old index entry with it.
147
+ */
148
+ export declare function searchTriggerStatements(): string[];
149
+ /**
150
+ * Create the full-text index and its triggers if they are not already there.
36
151
  *
37
152
  * A standalone (not external-content) FTS5 table. External content would store the text once rather
38
153
  * than twice, but it requires the indexed column names to exist verbatim on the content table, and
@@ -42,14 +157,25 @@ export declare const SEARCH_TABLE = "pithy_support_search";
42
157
  * `thread_id` and `message_id` are UNINDEXED: they are carried so a match resolves straight to a
43
158
  * conversation and so one message's rows can be replaced, and tokenizing a UUID would only add noise
44
159
  * to every query.
160
+ *
161
+ * **The triggers are dropped and recreated rather than guarded with `IF NOT EXISTS`**, which is what
162
+ * makes re-provisioning after a kit upgrade actually converge. `IF NOT EXISTS` is idempotent in the
163
+ * weak sense — it leaves a trigger whose body this release has changed exactly as it was, so a fixed
164
+ * body would reach nobody who already had the old one, and the command `pithy doctor` names would not
165
+ * be the command that fixes it. The pair is ordered so the table is always there before a body refers
166
+ * to it.
45
167
  */
46
168
  export declare function createSearchIndex(db: SupportDatabase | Kysely<unknown>): Promise<void>;
47
169
  /**
48
- * Drop the full-text index if it is there.
170
+ * Drop the full-text index and its triggers if they are there.
49
171
  *
50
172
  * Safe by construction: the index is derived, so this costs a rebuild rather than a restore. It is
51
173
  * what `pithy support provision` runs when `search.fts` goes back to false — and the reason turning
52
174
  * the feature off is now a one-command operation instead of a migration rollback.
175
+ *
176
+ * **The triggers go first, and the order is the whole point.** A trigger whose body inserts into a
177
+ * table that is gone fails every write to `pithy_support_messages`, so dropping the table out from
178
+ * under them would turn a feature flag into an outage on the capability's main write path.
53
179
  */
54
180
  export declare function dropSearchIndex(db: SupportDatabase | Kysely<unknown>): Promise<void>;
55
181
  //# sourceMappingURL=searchIndex.d.ts.map