@pithy-sh/support 0.7.0 → 0.8.1
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.
- package/README.md +1 -1
- package/dist/http/handlers.d.ts +2 -2
- package/dist/http/handlers.d.ts.map +1 -1
- package/dist/http/handlers.js +1 -1
- package/dist/http/responses.d.ts +4 -5
- package/dist/http/responses.d.ts.map +1 -1
- package/dist/http/responses.js +7 -10
- package/dist/http/routes.d.ts +3 -3
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +4 -3
- package/dist/http/schemas.d.ts +11 -9
- package/dist/http/schemas.d.ts.map +1 -1
- package/dist/http/schemas.js +14 -7
- package/dist/http/scopes.d.ts.map +1 -1
- package/dist/http/scopes.js +2 -1
- package/dist/store/threads.d.ts +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +8 -8
- package/pithy.manifest.json +1 -1
- package/src/http/handlers.ts +3 -3
- package/src/http/responses.ts +9 -12
- package/src/http/routes.ts +9 -8
- package/src/http/schemas.ts +19 -12
- package/src/http/scopes.ts +2 -1
- package/src/store/threads.ts +1 -1
- package/src/version.generated.ts +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ pithy add support
|
|
|
12
12
|
|
|
13
13
|
**A shared inbox.** Assign a conversation to somebody, filter the inbox by who has it, and see who else has a thread open before you reply: `POST /support/threads/:id/assign` behind `support:threads:assign`, `assignee=<ref>` or `assignee=none` on the listing, every handover kept in the thread's history, and one `support.thread` presence room per thread, with `viewing` and `replying`. Your own signed-in staff reach both through `support({ staff: { readPower, assignPower } })`, on unassignable roles other than the one ownership transfer confers, so only an operator can confer them. The seam is [`docs/CONTROL-PLANE.md`](../../docs/CONTROL-PLANE.md).
|
|
14
14
|
|
|
15
|
-
**Search, by words and by meaning.** `
|
|
15
|
+
**Search, by words and by meaning.** `POST /support/threads/search` takes, in its body, `q`, a `mode` (`keyword`, `semantic` or `hybrid`), a `limit` (20, at most 50) and the inbox list's filters, and returns ranked threads, each named by its best-matching message with that message's raw `bm25` and similarity. Keyword is FTS5 with `search.fts` on and the `LIKE` scan otherwise; hybrid fuses it with semantic search by rank, so a paraphrase is found and an order number still comes first. The inbox list no longer takes `q`. **Semantic search costs a second inference per inbound message on your own Workers AI binding**, on top of classification — roughly double the per-message AI cost — so `search: { semantic: true }` is off until you turn it on. `pithy support provision` creates the Vectorize index and backfills it once, and `pithy vector reprocess --index support-messages` re-embeds after a model change, with no `vector()` composed. Vectors carry message ids only. See [`docs/commands/support.md`](../../docs/commands/support.md#search).
|
|
16
16
|
|
|
17
17
|
**Replies come from the inbox.** A mailed reply is sent from the address the customer wrote to, under the name of whoever answered: `"Jim Scott" <support@help.example.com>`, with that inbox as the `Reply-To`. `reply.fromName` is the display name, with `{agent}` replaced by the agent's name (default `{agent}`); with no agent name, the email capability's `fromName` is used. A fixed `reply.fromName` with no `{agent}` is used for every reply. Support declares the addresses it sends as, `inboundAddresses` and `reply.replyToAddress`, through `email().enqueueAs` (`@pithy-sh/email` ^0.9.0; an older one is refused at compose). A thread whose inbox is no longer claimed is answered from `reply.replyToAddress`, or refused when none is set. A domain Email Service cannot send from fails the job with the domain named, and nothing is sent from another address in its place. `pithy doctor` names each sending domain, inbound or reply-to, that cannot send.
|
|
18
18
|
|
package/dist/http/handlers.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { type SenderPeers } from "../link/sender";
|
|
|
15
15
|
import { sendReply } from "../reply/send";
|
|
16
16
|
import type { SupportReplySnippets } from "../reply/snippets";
|
|
17
17
|
import type { SupportAssignResponse, SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportReplySentResponse, SupportReplyView, SupportSearchResponse, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse, SupportThreadView } from "./responses";
|
|
18
|
-
import type { ArchiveThreadInput, AssignThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput,
|
|
18
|
+
import type { ArchiveThreadInput, AssignThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SearchThreadsBody, SubmitFeedbackInput } from "./schemas";
|
|
19
19
|
/**
|
|
20
20
|
* The support handlers — every one of them behind the `control-plane` gate, and every one of them
|
|
21
21
|
* taking already-validated values.
|
|
@@ -71,7 +71,7 @@ export declare function listInbox(deps: HandlerDeps, query: ListThreadsQuery, vi
|
|
|
71
71
|
* Search the inbox: keyword and semantic, ranked as one. The semantic seams are whatever this Worker
|
|
72
72
|
* was given — absent bindings skip that backend and say so, rather than failing the search.
|
|
73
73
|
*/
|
|
74
|
-
export declare function searchInbox(deps: HandlerDeps, query:
|
|
74
|
+
export declare function searchInbox(deps: HandlerDeps, query: SearchThreadsBody, viewer: string): Promise<SupportSearchResponse>;
|
|
75
75
|
/** Read one conversation in full. */
|
|
76
76
|
export declare function readConversation(deps: HandlerDeps, threadId: string): Promise<SupportThreadResponse>;
|
|
77
77
|
/** Mark a conversation done, or reopen it. */
|
|
@@ -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;AAGnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAGpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAErE,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;AAQ9D,OAAO,KAAK,EACV,qBAAqB,EAErB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,2BAA2B,EAC3B,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"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"}
|
package/dist/http/handlers.js
CHANGED
|
@@ -55,7 +55,7 @@ async function searchInbox(deps, query, viewer) {
|
|
|
55
55
|
q: query.q,
|
|
56
56
|
...query.mode !== void 0 ? { mode: query.mode } : {},
|
|
57
57
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
58
|
-
archived: query.archived ===
|
|
58
|
+
archived: query.archived === true,
|
|
59
59
|
category: query.category,
|
|
60
60
|
declaredCategory: query.declaredCategory,
|
|
61
61
|
priority: query.priority,
|
package/dist/http/responses.d.ts
CHANGED
|
@@ -327,7 +327,7 @@ export declare const SupportSearchResultView: z.ZodObject<{
|
|
|
327
327
|
similarity: z.ZodOptional<z.ZodNumber>;
|
|
328
328
|
}, z.core.$strip>;
|
|
329
329
|
export type SupportSearchResultView = z.output<typeof SupportSearchResultView>;
|
|
330
|
-
/** `
|
|
330
|
+
/** `POST {base}/threads/search`. */
|
|
331
331
|
export declare const SupportSearchResponse: z.ZodObject<{
|
|
332
332
|
mode: z.ZodEnum<{
|
|
333
333
|
hybrid: "hybrid";
|
|
@@ -702,9 +702,8 @@ export type SupportFlagsResponse = z.output<typeof SupportFlagsResponse>;
|
|
|
702
702
|
* Below is the same projection for the other consumer: a management client reading a Worker **it does
|
|
703
703
|
* not control**. That client is crossing a trust boundary — a fork, a bug, a half-finished deploy, or a
|
|
704
704
|
* hostile Worker each put a member here that no enum of ours declares — and under the strict schema one
|
|
705
|
-
* such token costs it the entire response
|
|
706
|
-
*
|
|
707
|
-
* then had to widen our shape locally, which is the hand-held mirror `#113` exists to forbid.
|
|
705
|
+
* such token costs it the entire response: one unrecognized `channel` and a support pane renders none of
|
|
706
|
+
* its conversations, and the client is left widening our shape locally, the hand-held mirror the kit forbids.
|
|
708
707
|
*
|
|
709
708
|
* So the tolerance is published rather than reimplemented, by the one pattern
|
|
710
709
|
* `@pithy-sh/core/src/projection/asRead` states for the whole kit: same objects, every enum read as a
|
|
@@ -845,7 +844,7 @@ export declare const SupportThreadsResponseAsRead: z.ZodObject<{
|
|
|
845
844
|
nextCursor: z.ZodNullable<z.ZodString>;
|
|
846
845
|
}, z.core.$strip>;
|
|
847
846
|
export type SupportThreadsResponseAsRead = z.output<typeof SupportThreadsResponseAsRead>;
|
|
848
|
-
/** `
|
|
847
|
+
/** `POST {base}/threads/search`, as a client that does not control the Worker must read it. */
|
|
849
848
|
export declare const SupportSearchResponseAsRead: z.ZodObject<{
|
|
850
849
|
mode: z.ZodString;
|
|
851
850
|
backends: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/http/responses.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/http/responses.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiDxB,mEAAmE;AACnE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0F0C,CAAC;AACzE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAGkE,CAAC;AACvG,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE/E,qCAAqC;AACrC,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCoD,CAAC;AACpF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB;;;;;;;;;iBAqBuE,CAAC;AAC1G,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E,kFAAkF;AAClF,eAAO,MAAM,kBAAkB;;;;;;;;;iBAWiF,CAAC;AACjH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE,kFAAkF;AAClF,eAAO,MAAM,qBAAqB;;;;;iBAOiF,CAAC;AACpH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,sBAI5B,CAAC;AACJ,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;iBAU2E,CAAC;AAC1G,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB;;;;;iBAO8B,CAAC;AAC5D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE,4BAA4B;AAC5B,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKuB,CAAC;AAC3D,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAO7E,yBAAyB;AACzB,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqByE,CAAC;AAC9G,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE/E,oCAAoC;AACpC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqC2D,CAAC;AAC9F,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E,kEAAkE;AAClE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAiBiC,CAAC;AACpE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E,gCAAgC;AAChC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBkE,CAAC;AACrG,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA0E,CAAC;AAC9G,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB;;;;;;;8BAoBsD,CAAC;AAC5F,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBASmD,CAAC;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E,4CAA4C;AAC5C,eAAO,MAAM,2BAA2B;;iBAIwB,CAAC;AACjE,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEvF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;iBAEgC,CAAC;AAClE,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA4B,CAAC;AACjE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE/E,kGAAkG;AAClG,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAkC,CAAC;AAC7E,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAE3F,6FAA6F;AAC7F,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;iBAA6B,CAAC;AACnE,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,uFAAuF;AACvF,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiC,CAAC;AAC3E,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAEzF,+FAA+F;AAC/F,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgC,CAAC;AACzE,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEvF,2FAA2F;AAC3F,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgC,CAAC;AACzE,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEvF,oGAAoG;AACpG,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiC,CAAC;AAC3E,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAEzF,mGAAmG;AACnG,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgC,CAAC;AACzE,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEvF;;;;;;;;;;;;;;GAcG;AAEH,gDAAgD;AAChD,eAAO,MAAM,mBAAmB;;;;;;;iBAayE,CAAC;AAC1G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,uDAAuD;AACvD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBgE,CAAC;AAClG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEzE,8BAA8B;AAC9B,eAAO,MAAM,yBAAyB;;;;;iBAgBiB,CAAC;AACxD,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAEnF,6BAA6B;AAC7B,eAAO,MAAM,wBAAwB;;;;;;;;;;iBAK2B,CAAC;AACjE,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,iCAAiC;AACjC,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK0B,CAAC;AAC/D,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE/E,4BAA4B;AAC5B,eAAO,MAAM,sBAAsB;;;;;;;iBAEK,CAAC;AACzC,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
package/dist/http/responses.js
CHANGED
|
@@ -27,16 +27,14 @@ import { asRead } from "@pithy-sh/core/src/projection/asRead";
|
|
|
27
27
|
*
|
|
28
28
|
* **A browser imports this file, so it reaches no module that needs the Workers runtime.** That is not
|
|
29
29
|
* a preference about bundle size; it is what "a client validates what comes back" means once the client
|
|
30
|
-
* is a Vite program with the DOM lib and no Workers types
|
|
31
|
-
*
|
|
32
|
-
* which took `pithy-sh/dashboard`'s client build down on an error in `@pithy-sh/auth` (#419). The rule is
|
|
33
|
-
* held by `tooling/browser-scopes`, which compiles this module with `types: []` and asks the compiler
|
|
30
|
+
* is a Vite program with the DOM lib and no Workers types: an import of `link/sender.ts` would bring D1,
|
|
31
|
+
* Kysely and two capabilities' table maps into that build. The rule is held by `tooling/browser-scopes`, which compiles this module with `types: []` and asks the compiler
|
|
34
32
|
* which files that program included.
|
|
35
33
|
*
|
|
36
34
|
* **A field added here later is `.optional()`, not merely `.nullable()`.** This module is read across a
|
|
37
35
|
* version boundary — a management client validates a response with this schema against a customer's
|
|
38
36
|
* Worker at whatever kit version it is on — so an additive required key fails `safeParse` for everyone
|
|
39
|
-
* below that release and takes the whole pane with it
|
|
37
|
+
* below that release and takes the whole pane with it. Absent then means *this Worker cannot
|
|
40
38
|
* say*, which is a different fact from `null`.
|
|
41
39
|
*/
|
|
42
40
|
/** Where a page resumes, or the end of the list. */
|
|
@@ -186,7 +184,7 @@ const SupportSearchResultView = z.object({
|
|
|
186
184
|
bm25: z.number().optional().describe("That message's FTS5 `bm25`, raw: lower is better, and comparable only within one search. Absent when FTS5 did not run or did not match it — including on the `LIKE` scan, which has no score."),
|
|
187
185
|
similarity: z.number().optional().describe("That message's similarity to the query in the semantic index, raw: higher is closer. Absent when semantic search did not run or did not match it. Never normalized against `bm25` — the two are reported as they are.")
|
|
188
186
|
}).describe("One thread a search found, named by its best-matching message, with that message's raw scores.");
|
|
189
|
-
/** `
|
|
187
|
+
/** `POST {base}/threads/search`. */
|
|
190
188
|
const SupportSearchResponse = z.object({
|
|
191
189
|
mode: z.enum([
|
|
192
190
|
"keyword",
|
|
@@ -278,9 +276,8 @@ const SupportFlagsResponse = z.object({ ok: z.literal(true).describe("Always tru
|
|
|
278
276
|
* Below is the same projection for the other consumer: a management client reading a Worker **it does
|
|
279
277
|
* not control**. That client is crossing a trust boundary — a fork, a bug, a half-finished deploy, or a
|
|
280
278
|
* hostile Worker each put a member here that no enum of ours declares — and under the strict schema one
|
|
281
|
-
* such token costs it the entire response
|
|
282
|
-
*
|
|
283
|
-
* then had to widen our shape locally, which is the hand-held mirror `#113` exists to forbid.
|
|
279
|
+
* such token costs it the entire response: one unrecognized `channel` and a support pane renders none of
|
|
280
|
+
* its conversations, and the client is left widening our shape locally, the hand-held mirror the kit forbids.
|
|
284
281
|
*
|
|
285
282
|
* So the tolerance is published rather than reimplemented, by the one pattern
|
|
286
283
|
* `@pithy-sh/core/src/projection/asRead` states for the whole kit: same objects, every enum read as a
|
|
@@ -304,7 +301,7 @@ const SupportListedThreadViewAsRead = asRead(SupportListedThreadView);
|
|
|
304
301
|
const SupportMessageViewAsRead = asRead(SupportMessageView);
|
|
305
302
|
/** `GET {base}/threads`, as a client that does not control the Worker must read it. */
|
|
306
303
|
const SupportThreadsResponseAsRead = asRead(SupportThreadsResponse);
|
|
307
|
-
/** `
|
|
304
|
+
/** `POST {base}/threads/search`, as a client that does not control the Worker must read it. */
|
|
308
305
|
const SupportSearchResponseAsRead = asRead(SupportSearchResponse);
|
|
309
306
|
/** `GET {base}/threads/:id`, as a client that does not control the Worker must read it. */
|
|
310
307
|
const SupportThreadResponseAsRead = asRead(SupportThreadResponse);
|
package/dist/http/routes.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { type HandlerDeps } from "./handlers";
|
|
|
8
8
|
* The support routes, their verification strategies, and what each accepts:
|
|
9
9
|
*
|
|
10
10
|
* GET /support/threads → the inbox (control-plane: support:threads:read) query: ListThreadsQuery
|
|
11
|
-
*
|
|
11
|
+
* POST /support/threads/search → search (control-plane: support:threads:read) json: SearchThreadsBody
|
|
12
12
|
* GET /support/threads/:id → one thread (control-plane: support:threads:read) param: ThreadIdParam
|
|
13
13
|
* POST /support/threads/:id/archive → done/reopen (control-plane: support:threads:archive) param + json
|
|
14
14
|
* POST /support/threads/:id/assign → who has it (control-plane: support:threads:assign) param + json
|
|
@@ -58,8 +58,8 @@ import { type HandlerDeps } from "./handlers";
|
|
|
58
58
|
* person who most needs to reach support is often the one whose access is broken, and a role the kit
|
|
59
59
|
* invented would make one adopter's account model a condition on everybody's ability to report a bug.
|
|
60
60
|
*
|
|
61
|
-
* But an adopter whose *own* model makes some submissions act-on-behalf-of —
|
|
62
|
-
*
|
|
61
|
+
* But an adopter whose *own* model makes some submissions act-on-behalf-of — a discount application made
|
|
62
|
+
* for an organization, which a member may not make — needs somewhere to
|
|
63
63
|
* put that check, and it must not be the client. **The seam for it already exists, in the composition
|
|
64
64
|
* contract rather than in this capability's config**, and it is documented here because it was not
|
|
65
65
|
* discoverable rather than because it was missing:
|
|
@@ -1 +1 @@
|
|
|
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;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAGL,KAAK,WAAW,EAWjB,MAAM,YAAY,CAAC;AAoCpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkGG;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;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAuCD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,
|
|
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;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAGL,KAAK,WAAW,EAWjB,MAAM,YAAY,CAAC;AAoCpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkGG;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;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAuCD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,CAsNtG"}
|
package/dist/http/routes.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { requireAuth, requireStaff } from "./guards.js";
|
|
4
4
|
import { archiveConversation, assignConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, searchInbox, submitFeedbackRequest, updateFlags } from "./handlers.js";
|
|
5
5
|
import { SupportArchiveResponse, SupportAssignResponse, SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportRepliesResponse, SupportReplySentResponse, SupportSearchResponse, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse } from "./responses.js";
|
|
6
|
-
import { ArchiveThreadInput, AssignThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput,
|
|
6
|
+
import { ArchiveThreadInput, AssignThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SearchThreadsBody, SubmitFeedbackInput, ThreadIdParam } from "./schemas.js";
|
|
7
7
|
import { SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_ASSIGN_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE } from "./scopes.js";
|
|
8
8
|
import { InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
9
9
|
import { requireControlPlane } from "@pithy-sh/core/src/controlPlane/http/guard";
|
|
@@ -54,12 +54,13 @@ function registerSupportRoutes(options) {
|
|
|
54
54
|
scopes: [SUPPORT_THREADS_READ_SCOPE],
|
|
55
55
|
responses: { 200: SupportThreadsResponse }
|
|
56
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.
|
|
57
|
+
app.post(`${base}/threads/search`, describeRoute({
|
|
58
58
|
summary: "Search the inbox, by words and by meaning, ranked as one.",
|
|
59
|
+
description: "The words travel in the body, because they are often a customer's address or order number and a query string is written to every request log between the client and the Worker. Read-only.",
|
|
59
60
|
verification: "control-plane",
|
|
60
61
|
scopes: [SUPPORT_THREADS_READ_SCOPE],
|
|
61
62
|
responses: { 200: SupportSearchResponse }
|
|
62
|
-
}), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("
|
|
63
|
+
}), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("json", SearchThreadsBody), async (c) => c.json(await searchInbox(await resolve(c), c.req.valid("json"), viewer(c))));
|
|
63
64
|
app.get(`${base}/replies`, describeRoute({
|
|
64
65
|
summary: "List the canned replies.",
|
|
65
66
|
verification: "control-plane",
|
package/dist/http/schemas.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type ThreadIdParam = z.infer<typeof ThreadIdParam>;
|
|
|
21
21
|
/**
|
|
22
22
|
* The inbox listing query.
|
|
23
23
|
*
|
|
24
|
-
* **Strict, and there is no `q`.** Listing lists; `
|
|
24
|
+
* **Strict, and there is no `q`.** Listing lists; `POST {basePath}/threads/search` searches. An unknown
|
|
25
25
|
* key is refused as `validation/invalid_input` rather than ignored, so a client sending `q` is told so
|
|
26
26
|
* instead of getting the unfiltered inbox back as though it were the answer.
|
|
27
27
|
*/
|
|
@@ -60,12 +60,13 @@ export declare const SupportSearchMode: z.ZodEnum<{
|
|
|
60
60
|
semantic: "semantic";
|
|
61
61
|
}>;
|
|
62
62
|
export type SupportSearchMode = z.infer<typeof SupportSearchMode>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The body of `POST {basePath}/threads/search`: the words, the mode, how many, and the inbox listing's filters.
|
|
65
|
+
* A body, not a query string, because the words are often a customer's address or order number and a URL is
|
|
66
|
+
* written to every request log between the client and the Worker. Strict, so a misspelled filter is refused
|
|
67
|
+
* rather than silently widening the search.
|
|
68
|
+
*/
|
|
69
|
+
export declare const SearchThreadsBody: z.ZodObject<{
|
|
69
70
|
category: z.ZodOptional<z.ZodString>;
|
|
70
71
|
declaredCategory: z.ZodOptional<z.ZodString>;
|
|
71
72
|
priority: z.ZodOptional<z.ZodEnum<{
|
|
@@ -91,9 +92,10 @@ export declare const SearchThreadsQuery: z.ZodObject<{
|
|
|
91
92
|
keyword: "keyword";
|
|
92
93
|
semantic: "semantic";
|
|
93
94
|
}>>;
|
|
94
|
-
limit: z.ZodOptional<z.
|
|
95
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
96
|
+
archived: z.ZodOptional<z.ZodBoolean>;
|
|
95
97
|
}, z.core.$strict>;
|
|
96
|
-
export type
|
|
98
|
+
export type SearchThreadsBody = z.infer<typeof SearchThreadsBody>;
|
|
97
99
|
/** The archive/unarchive body. */
|
|
98
100
|
export declare const ArchiveThreadInput: z.ZodObject<{
|
|
99
101
|
archived: z.ZodBoolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/http/schemas.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/http/schemas.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB;;;;;;;;;;;GAWG;AAEH,+BAA+B;AAC/B,eAAO,MAAM,aAAa;;iBAOsC,CAAC;AACjE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAyC1D;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiBmE,CAAC;AACjG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,yBAAyB;AACzB,eAAO,MAAM,iBAAiB;;;;EAI3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwBsF,CAAC;AACrH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,kCAAkC;AAClC,eAAO,MAAM,kBAAkB;;iBAQuB,CAAC;AACvD,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,mCAAmC;AACnC,eAAO,MAAM,aAAa;;;iBAOoE,CAAC;AAC/F,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,iGAAiG;AACjG,eAAO,MAAM,iBAAiB;;;;;;iBAeiC,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,sBAAsB;AACtB,eAAO,MAAM,UAAU;;;iBAmB0B,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,iCAAiC;AACjC,eAAO,MAAM,UAAU;;;iBAWmC,CAAC;AAC3D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;;;iBA4B8D,CAAC;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;iBAiCqF,CAAC;AACtH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uCAAuC;AACvC,eAAO,MAAM,cAAc;;;iBAiBxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,sCAAsC;AACtC,eAAO,MAAM,YAAY;;iBAM4B,CAAC;AACtD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC"}
|
package/dist/http/schemas.js
CHANGED
|
@@ -6,6 +6,7 @@ import { UNASSIGNED } from "../data/assignment.js";
|
|
|
6
6
|
import { SupportSubmissionContext } from "../data/message.js";
|
|
7
7
|
import "../store/paging.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { freeText } from "@pithy-sh/core/src/http/freeText";
|
|
9
10
|
import { PresenceParticipantName, PresenceParticipantRef } from "@pithy-sh/core/src/presence/frames";
|
|
10
11
|
//#region src/http/schemas.ts
|
|
11
12
|
/** An assignee reference, as the listing filters on it and an assignment names it. */
|
|
@@ -40,7 +41,7 @@ const threadFilterFields = {
|
|
|
40
41
|
/**
|
|
41
42
|
* The inbox listing query.
|
|
42
43
|
*
|
|
43
|
-
* **Strict, and there is no `q`.** Listing lists; `
|
|
44
|
+
* **Strict, and there is no `q`.** Listing lists; `POST {basePath}/threads/search` searches. An unknown
|
|
44
45
|
* key is refused as `validation/invalid_input` rather than ignored, so a client sending `q` is told so
|
|
45
46
|
* instead of getting the unfiltered inbox back as though it were the answer.
|
|
46
47
|
*/
|
|
@@ -55,12 +56,18 @@ const SupportSearchMode = z.enum([
|
|
|
55
56
|
"semantic",
|
|
56
57
|
"hybrid"
|
|
57
58
|
]).describe("`keyword` finds the exact words — FTS5 when `search.fts` is on, the `LIKE` scan otherwise. `semantic` finds the meaning, through the message index. `hybrid` runs both and ranks them as one.");
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The body of `POST {basePath}/threads/search`: the words, the mode, how many, and the inbox listing's filters.
|
|
61
|
+
* A body, not a query string, because the words are often a customer's address or order number and a URL is
|
|
62
|
+
* written to every request log between the client and the Worker. Strict, so a misspelled filter is refused
|
|
63
|
+
* rather than silently widening the search.
|
|
64
|
+
*/
|
|
65
|
+
const SearchThreadsBody = z.object({
|
|
66
|
+
...threadFilterFields,
|
|
67
|
+
q: freeText(z.string().trim().min(1).max(200)).describe("What to search for, in the customer's words or yours. An order number, an error, or a description."),
|
|
61
68
|
mode: SupportSearchMode.optional().describe("Which backends to run. Absent runs every one that is on, as `hybrid`. `semantic` or `hybrid` while `search.semantic` is off is refused, naming the flag."),
|
|
62
|
-
limit: z.
|
|
63
|
-
|
|
69
|
+
limit: z.number().int().min(1).max(50).optional().describe(`How many threads to return, best first. Default 20, at most 50. No cursor: a ranked list has a top, not pages.`),
|
|
70
|
+
archived: z.boolean().optional().describe("`true` for the done pile, `false` or absent for the open inbox — which is the default on purpose.")
|
|
64
71
|
}).strict().describe("A search over the inbox: the words, the mode, how many, and the same filters the inbox listing takes.");
|
|
65
72
|
/** The archive/unarchive body. */
|
|
66
73
|
const ArchiveThreadInput = z.object({ archived: z.boolean().describe("`true` marks the conversation done; `false` reopens it. Explicit rather than a toggle, so a retried request lands on the state the caller meant rather than flipping it back.") }).describe("Mark a conversation done, or reopen it.");
|
|
@@ -127,4 +134,4 @@ const MyThreadsQuery = z.object({
|
|
|
127
134
|
/** The canned-reply catalog query. */
|
|
128
135
|
const RepliesQuery = z.object({ category: CategoryFilter.optional().describe("Order the catalog for this category — its snippets first, then the general-purpose ones. Ordering, never filtering: a misclassified thread must not hide the snippet its operator actually needs.") }).describe("How to order the canned reply catalog.");
|
|
129
136
|
//#endregion
|
|
130
|
-
export { ArchiveThreadInput, AssignThreadInput, AssigneeInput, FlagsInput, ListThreadsQuery, MAX_ASSIGNMENT_NOTE_CHARS, MyThreadsQuery, RepliesQuery, ReplyInput,
|
|
137
|
+
export { ArchiveThreadInput, AssignThreadInput, AssigneeInput, FlagsInput, ListThreadsQuery, MAX_ASSIGNMENT_NOTE_CHARS, MyThreadsQuery, RepliesQuery, ReplyInput, SearchThreadsBody, SubmitFeedbackInput, SubmittedAttachmentInput, SupportSearchMode, ThreadIdParam };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/http/scopes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sDAAsD,CAAC;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6CAA6C,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,EAAE,iBAA0C,CAAC;AAEpF,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,EAAE,iBAA6C,CAAC;AAE1F;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,iBAA2C,CAAC;AAEtF,8EAA8E;AAC9E,eAAO,MAAM,gCAAgC,EAAE,iBAAgD,CAAC;AAEhG,wGAAwG;AACxG,eAAO,MAAM,0BAA0B,EAAE,iBAA0C,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,iBAA4C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,iBAAiB,EAOpE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,EAAE,
|
|
1
|
+
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/http/scopes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sDAAsD,CAAC;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6CAA6C,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,EAAE,iBAA0C,CAAC;AAEpF,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,EAAE,iBAA6C,CAAC;AAE1F;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,iBAA2C,CAAC;AAEtF,8EAA8E;AAC9E,eAAO,MAAM,gCAAgC,EAAE,iBAAgD,CAAC;AAEhG,wGAAwG;AACxG,eAAO,MAAM,0BAA0B,EAAE,iBAA0C,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,iBAA4C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,iBAAiB,EAOpE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,EAAE,CA4E5E"}
|
package/dist/http/scopes.js
CHANGED
|
@@ -76,11 +76,12 @@ function supportAdminRoutes(basePath) {
|
|
|
76
76
|
summary: "List the inbox, newest first, filtered by category, priority, assignee, and archived."
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
|
-
method: "
|
|
79
|
+
method: "POST",
|
|
80
80
|
operation: "threads.search",
|
|
81
81
|
path: `${basePath}/threads/search`,
|
|
82
82
|
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
83
83
|
kind: "reads",
|
|
84
|
+
readInBody: true,
|
|
84
85
|
summary: "Search the inbox by words and by meaning, ranked as one, with the same filters as the list."
|
|
85
86
|
},
|
|
86
87
|
{
|
package/dist/store/threads.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface ThreadFilters {
|
|
|
66
66
|
/**
|
|
67
67
|
* What a caller may filter the inbox by, and where to resume.
|
|
68
68
|
*
|
|
69
|
-
* **No free text.** Listing lists; `
|
|
69
|
+
* **No free text.** Listing lists; `POST {basePath}/threads/search` searches, ranked, with a limit and no
|
|
70
70
|
* cursor. A `q` here could say whether a thread matched, never how well.
|
|
71
71
|
*/
|
|
72
72
|
export interface ListThreadsQuery extends ThreadFilters {
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
/** This package's npm name — the join key against a release feed. */
|
|
4
4
|
export declare const PACKAGE_NAME = "@pithy-sh/support";
|
|
5
5
|
/** This package's version, stamped from its own package.json at generation time. */
|
|
6
|
-
export declare const PACKAGE_VERSION = "0.
|
|
6
|
+
export declare const PACKAGE_VERSION = "0.8.1";
|
|
7
7
|
//# sourceMappingURL=version.generated.d.ts.map
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
/** This package's npm name — the join key against a release feed. */
|
|
5
5
|
const PACKAGE_NAME = "@pithy-sh/support";
|
|
6
6
|
/** This package's version, stamped from its own package.json at generation time. */
|
|
7
|
-
const PACKAGE_VERSION = "0.
|
|
7
|
+
const PACKAGE_VERSION = "0.8.1";
|
|
8
8
|
//#endregion
|
|
9
9
|
export { PACKAGE_NAME, PACKAGE_VERSION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/support",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"tldts": "^7.4.10"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@pithy-sh/auth": "^0.
|
|
44
|
-
"@pithy-sh/core": "^0.
|
|
45
|
-
"@pithy-sh/email": "^0.
|
|
46
|
-
"@pithy-sh/payments": "^0.8.
|
|
47
|
-
"@pithy-sh/secrets": "^0.6.
|
|
48
|
-
"@pithy-sh/storage": "^0.7.
|
|
49
|
-
"@pithy-sh/vector": "^0.5.
|
|
43
|
+
"@pithy-sh/auth": "^0.14.1",
|
|
44
|
+
"@pithy-sh/core": "^0.19.0",
|
|
45
|
+
"@pithy-sh/email": "^0.10.1",
|
|
46
|
+
"@pithy-sh/payments": "^0.8.2",
|
|
47
|
+
"@pithy-sh/secrets": "^0.6.7",
|
|
48
|
+
"@pithy-sh/storage": "^0.7.5",
|
|
49
|
+
"@pithy-sh/vector": "^0.5.7",
|
|
50
50
|
"hono": "^4.13.2",
|
|
51
51
|
"kysely": "^0.29.0",
|
|
52
52
|
"zod": "^4.4.0"
|
package/pithy.manifest.json
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"If your feedback screen offers a chooser, send its answer as `declaredCategory`. It lands in its own column beside `category` and is never overwritten by the classifier — one is a claim by the person writing, the other a judgement about them, and an operator triaging an inbox needs to tell them apart. `GET /support/threads` filters on either. A key outside your taxonomy is refused rather than stored, and a category sent with a `threadId` is refused too: a chooser belongs on the form that opens a request.",
|
|
46
46
|
"`POST /support/feedback` is gated on a session and same-origin and never on a role — a general intake that is role-gated is not one. If your own account model makes some submissions act-on-behalf-of, put that check in your `app` capability's `middleware` over your own `basePath`: every capability's middleware mounts before any capability's routes, so yours runs after auth has resolved the session and before this capability's `requireAuth()`.",
|
|
47
47
|
"If your own signed-in users work the inbox, name two powers in `support({ staff: { readPower, assignPower } })` and give them to a role in your organization catalog's `unassignable` list that `organization({ ownership })` does not confer, so only an operator can confer them — anybody may found an organization and take its ownership role, so `support()` refuses a staff power there. `readPower` lets a staff member join a thread's presence room; `assignPower` lets them assign it at `POST /support/staff/threads/:id/assign`. Without `staff`, both answer to management clients only.",
|
|
48
|
-
"Search the inbox at `
|
|
48
|
+
"Search the inbox at `POST /support/threads/search`, with the words and the filters in a JSON body, so a customer's address typed into the box never lands in a request log — keyword by default, ranked; the inbox list takes no `q`. Turn on `search: { semantic: true }` to find a paraphrase as well as an exact word: it embeds every inbound message on your own Workers AI binding, a second inference per message on top of classification, so it is off until you choose it. Then run `pithy support provision`, which creates the Vectorize index, binds it, and backfills it once; `pithy vector reprocess --index support-messages` re-embeds after a model change.",
|
|
49
49
|
"Run `pithy support provision` after adding support or upgrading it: `search.fts` is on by default, and the FTS5 index and its triggers are provisioned rather than migrated. Until then search falls back to a `LIKE` scan and `pithy doctor` names the environment. Back the database up with `pithy backup`, which skips the index, and restore it with `pithy restore`, which rebuilds it — a plain `wrangler d1 export` fails on any database holding an FTS5 table."
|
|
50
50
|
],
|
|
51
51
|
"configOptions": [
|
package/src/http/handlers.ts
CHANGED
|
@@ -56,7 +56,7 @@ import type {
|
|
|
56
56
|
MyThreadsQuery,
|
|
57
57
|
RepliesQuery,
|
|
58
58
|
ReplyInput,
|
|
59
|
-
|
|
59
|
+
SearchThreadsBody,
|
|
60
60
|
SubmitFeedbackInput,
|
|
61
61
|
} from "./schemas";
|
|
62
62
|
import {
|
|
@@ -148,7 +148,7 @@ export async function listInbox(
|
|
|
148
148
|
*/
|
|
149
149
|
export async function searchInbox(
|
|
150
150
|
deps: HandlerDeps,
|
|
151
|
-
query:
|
|
151
|
+
query: SearchThreadsBody,
|
|
152
152
|
viewer: string,
|
|
153
153
|
): Promise<SupportSearchResponse> {
|
|
154
154
|
const outcome = await searchThreads(
|
|
@@ -165,7 +165,7 @@ export async function searchInbox(
|
|
|
165
165
|
q: query.q,
|
|
166
166
|
...(query.mode !== undefined ? { mode: query.mode } : {}),
|
|
167
167
|
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
168
|
-
archived: query.archived ===
|
|
168
|
+
archived: query.archived === true,
|
|
169
169
|
category: query.category,
|
|
170
170
|
declaredCategory: query.declaredCategory,
|
|
171
171
|
priority: query.priority,
|
package/src/http/responses.ts
CHANGED
|
@@ -34,16 +34,14 @@ import { SupportSubmissionContext } from "../data/message";
|
|
|
34
34
|
*
|
|
35
35
|
* **A browser imports this file, so it reaches no module that needs the Workers runtime.** That is not
|
|
36
36
|
* a preference about bundle size; it is what "a client validates what comes back" means once the client
|
|
37
|
-
* is a Vite program with the DOM lib and no Workers types
|
|
38
|
-
*
|
|
39
|
-
* which took `pithy-sh/dashboard`'s client build down on an error in `@pithy-sh/auth` (#419). The rule is
|
|
40
|
-
* held by `tooling/browser-scopes`, which compiles this module with `types: []` and asks the compiler
|
|
37
|
+
* is a Vite program with the DOM lib and no Workers types: an import of `link/sender.ts` would bring D1,
|
|
38
|
+
* Kysely and two capabilities' table maps into that build. The rule is held by `tooling/browser-scopes`, which compiles this module with `types: []` and asks the compiler
|
|
41
39
|
* which files that program included.
|
|
42
40
|
*
|
|
43
41
|
* **A field added here later is `.optional()`, not merely `.nullable()`.** This module is read across a
|
|
44
42
|
* version boundary — a management client validates a response with this schema against a customer's
|
|
45
43
|
* Worker at whatever kit version it is on — so an additive required key fails `safeParse` for everyone
|
|
46
|
-
* below that release and takes the whole pane with it
|
|
44
|
+
* below that release and takes the whole pane with it. Absent then means *this Worker cannot
|
|
47
45
|
* say*, which is a different fact from `null`.
|
|
48
46
|
*/
|
|
49
47
|
|
|
@@ -110,7 +108,7 @@ export const SupportThreadView = z
|
|
|
110
108
|
.describe(
|
|
111
109
|
"The control-plane subject that archived it, for convenience only — the answer to 'who marked this done' comes from the audit trail.",
|
|
112
110
|
),
|
|
113
|
-
// Optional as well as nullable
|
|
111
|
+
// Optional as well as nullable: absent says *this Worker predates assignment*, which a client
|
|
114
112
|
// must not read as *nobody has it*.
|
|
115
113
|
assigneeRef: z
|
|
116
114
|
.string()
|
|
@@ -346,7 +344,7 @@ export const SupportSearchResultView = z
|
|
|
346
344
|
.describe("One thread a search found, named by its best-matching message, with that message's raw scores.");
|
|
347
345
|
export type SupportSearchResultView = z.output<typeof SupportSearchResultView>;
|
|
348
346
|
|
|
349
|
-
/** `
|
|
347
|
+
/** `POST {base}/threads/search`. */
|
|
350
348
|
export const SupportSearchResponse = z
|
|
351
349
|
.object({
|
|
352
350
|
mode: z
|
|
@@ -418,7 +416,7 @@ export const SupportThreadResponse = z
|
|
|
418
416
|
.describe("Its attachments, across every message, each naming the message it arrived on in `messageId`."),
|
|
419
417
|
sender: SenderContextView.describe("What the app already knows about the sender."),
|
|
420
418
|
replies: z.array(SupportReplyView).describe("The canned replies, ordered for this thread's category."),
|
|
421
|
-
// Optional
|
|
419
|
+
// Optional: absent from a Worker that predates assignment.
|
|
422
420
|
assignments: z
|
|
423
421
|
.array(SupportAssignmentView)
|
|
424
422
|
.optional()
|
|
@@ -518,9 +516,8 @@ export type SupportFlagsResponse = z.output<typeof SupportFlagsResponse>;
|
|
|
518
516
|
* Below is the same projection for the other consumer: a management client reading a Worker **it does
|
|
519
517
|
* not control**. That client is crossing a trust boundary — a fork, a bug, a half-finished deploy, or a
|
|
520
518
|
* hostile Worker each put a member here that no enum of ours declares — and under the strict schema one
|
|
521
|
-
* such token costs it the entire response
|
|
522
|
-
*
|
|
523
|
-
* then had to widen our shape locally, which is the hand-held mirror `#113` exists to forbid.
|
|
519
|
+
* such token costs it the entire response: one unrecognized `channel` and a support pane renders none of
|
|
520
|
+
* its conversations, and the client is left widening our shape locally, the hand-held mirror the kit forbids.
|
|
524
521
|
*
|
|
525
522
|
* So the tolerance is published rather than reimplemented, by the one pattern
|
|
526
523
|
* `@pithy-sh/core/src/projection/asRead` states for the whole kit: same objects, every enum read as a
|
|
@@ -553,7 +550,7 @@ export type SupportMessageViewAsRead = z.output<typeof SupportMessageViewAsRead>
|
|
|
553
550
|
export const SupportThreadsResponseAsRead = asRead(SupportThreadsResponse);
|
|
554
551
|
export type SupportThreadsResponseAsRead = z.output<typeof SupportThreadsResponseAsRead>;
|
|
555
552
|
|
|
556
|
-
/** `
|
|
553
|
+
/** `POST {base}/threads/search`, as a client that does not control the Worker must read it. */
|
|
557
554
|
export const SupportSearchResponseAsRead = asRead(SupportSearchResponse);
|
|
558
555
|
export type SupportSearchResponseAsRead = z.output<typeof SupportSearchResponseAsRead>;
|
|
559
556
|
|
package/src/http/routes.ts
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
MyThreadsQuery,
|
|
48
48
|
RepliesQuery,
|
|
49
49
|
ReplyInput,
|
|
50
|
-
|
|
50
|
+
SearchThreadsBody,
|
|
51
51
|
SubmitFeedbackInput,
|
|
52
52
|
ThreadIdParam,
|
|
53
53
|
} from "./schemas";
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
* The support routes, their verification strategies, and what each accepts:
|
|
65
65
|
*
|
|
66
66
|
* GET /support/threads → the inbox (control-plane: support:threads:read) query: ListThreadsQuery
|
|
67
|
-
*
|
|
67
|
+
* POST /support/threads/search → search (control-plane: support:threads:read) json: SearchThreadsBody
|
|
68
68
|
* GET /support/threads/:id → one thread (control-plane: support:threads:read) param: ThreadIdParam
|
|
69
69
|
* POST /support/threads/:id/archive → done/reopen (control-plane: support:threads:archive) param + json
|
|
70
70
|
* POST /support/threads/:id/assign → who has it (control-plane: support:threads:assign) param + json
|
|
@@ -114,8 +114,8 @@ import {
|
|
|
114
114
|
* person who most needs to reach support is often the one whose access is broken, and a role the kit
|
|
115
115
|
* invented would make one adopter's account model a condition on everybody's ability to report a bug.
|
|
116
116
|
*
|
|
117
|
-
* But an adopter whose *own* model makes some submissions act-on-behalf-of —
|
|
118
|
-
*
|
|
117
|
+
* But an adopter whose *own* model makes some submissions act-on-behalf-of — a discount application made
|
|
118
|
+
* for an organization, which a member may not make — needs somewhere to
|
|
119
119
|
* put that check, and it must not be the client. **The seam for it already exists, in the composition
|
|
120
120
|
* contract rather than in this capability's config**, and it is documented here because it was not
|
|
121
121
|
* discoverable rather than because it was missing:
|
|
@@ -238,18 +238,19 @@ export function registerSupportRoutes(options: SupportRoutesOptions): (app: Hono
|
|
|
238
238
|
async (c) => c.json(await listInbox(await resolve(c), c.req.valid("query"), viewer(c))),
|
|
239
239
|
);
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
app.get(
|
|
241
|
+
app.post(
|
|
243
242
|
`${base}/threads/search`,
|
|
244
243
|
describeRoute({
|
|
245
244
|
summary: "Search the inbox, by words and by meaning, ranked as one.",
|
|
245
|
+
description:
|
|
246
|
+
"The words travel in the body, because they are often a customer's address or order number and a query string is written to every request log between the client and the Worker. Read-only.",
|
|
246
247
|
verification: "control-plane",
|
|
247
248
|
scopes: [SUPPORT_THREADS_READ_SCOPE],
|
|
248
249
|
responses: { 200: SupportSearchResponse },
|
|
249
250
|
}),
|
|
250
251
|
requireControlPlane(SUPPORT_THREADS_READ_SCOPE),
|
|
251
|
-
validate("
|
|
252
|
-
async (c) => c.json(await searchInbox(await resolve(c), c.req.valid("
|
|
252
|
+
validate("json", SearchThreadsBody),
|
|
253
|
+
async (c) => c.json(await searchInbox(await resolve(c), c.req.valid("json"), viewer(c))),
|
|
253
254
|
);
|
|
254
255
|
|
|
255
256
|
app.get(
|
package/src/http/schemas.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
import { freeText } from "@pithy-sh/core/src/http/freeText";
|
|
4
5
|
import { PresenceParticipantName, PresenceParticipantRef } from "@pithy-sh/core/src/presence/frames";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { MAX_SUBMISSION_ATTACHMENTS, MAX_SUBMISSION_BODY_CHARS, MAX_SUBMISSION_SUBJECT_CHARS } from "../config/config";
|
|
@@ -82,7 +83,7 @@ const threadFilterFields = {
|
|
|
82
83
|
/**
|
|
83
84
|
* The inbox listing query.
|
|
84
85
|
*
|
|
85
|
-
* **Strict, and there is no `q`.** Listing lists; `
|
|
86
|
+
* **Strict, and there is no `q`.** Listing lists; `POST {basePath}/threads/search` searches. An unknown
|
|
86
87
|
* key is refused as `validation/invalid_input` rather than ignored, so a client sending `q` is told so
|
|
87
88
|
* instead of getting the unfiltered inbox back as though it were the answer.
|
|
88
89
|
*/
|
|
@@ -114,19 +115,22 @@ export const SupportSearchMode = z
|
|
|
114
115
|
);
|
|
115
116
|
export type SupportSearchMode = z.infer<typeof SupportSearchMode>;
|
|
116
117
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The body of `POST {basePath}/threads/search`: the words, the mode, how many, and the inbox listing's filters.
|
|
120
|
+
* A body, not a query string, because the words are often a customer's address or order number and a URL is
|
|
121
|
+
* written to every request log between the client and the Worker. Strict, so a misspelled filter is refused
|
|
122
|
+
* rather than silently widening the search.
|
|
123
|
+
*/
|
|
124
|
+
export const SearchThreadsBody = z
|
|
119
125
|
.object({
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
.max(MAX_SEARCH_TERM)
|
|
125
|
-
.describe("What to search for, in the customer's words or yours. An order number, an error, or a description."),
|
|
126
|
+
...threadFilterFields,
|
|
127
|
+
q: freeText(z.string().trim().min(1).max(MAX_SEARCH_TERM)).describe(
|
|
128
|
+
"What to search for, in the customer's words or yours. An order number, an error, or a description.",
|
|
129
|
+
),
|
|
126
130
|
mode: SupportSearchMode.optional().describe(
|
|
127
131
|
"Which backends to run. Absent runs every one that is on, as `hybrid`. `semantic` or `hybrid` while `search.semantic` is off is refused, naming the flag.",
|
|
128
132
|
),
|
|
129
|
-
limit: z
|
|
133
|
+
limit: z
|
|
130
134
|
.number()
|
|
131
135
|
.int()
|
|
132
136
|
.min(1)
|
|
@@ -135,11 +139,14 @@ export const SearchThreadsQuery = z
|
|
|
135
139
|
.describe(
|
|
136
140
|
`How many threads to return, best first. Default ${DEFAULT_SEARCH_LIMIT}, at most ${MAX_SEARCH_LIMIT}. No cursor: a ranked list has a top, not pages.`,
|
|
137
141
|
),
|
|
138
|
-
|
|
142
|
+
archived: z
|
|
143
|
+
.boolean()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("`true` for the done pile, `false` or absent for the open inbox — which is the default on purpose."),
|
|
139
146
|
})
|
|
140
147
|
.strict()
|
|
141
148
|
.describe("A search over the inbox: the words, the mode, how many, and the same filters the inbox listing takes.");
|
|
142
|
-
export type
|
|
149
|
+
export type SearchThreadsBody = z.infer<typeof SearchThreadsBody>;
|
|
143
150
|
|
|
144
151
|
/** The archive/unarchive body. */
|
|
145
152
|
export const ArchiveThreadInput = z
|
package/src/http/scopes.ts
CHANGED
|
@@ -87,11 +87,12 @@ export function supportAdminRoutes(basePath: string): AdminRouteDeclaration[] {
|
|
|
87
87
|
summary: "List the inbox, newest first, filtered by category, priority, assignee, and archived.",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
|
-
method: "
|
|
90
|
+
method: "POST",
|
|
91
91
|
operation: "threads.search",
|
|
92
92
|
path: `${basePath}/threads/search`,
|
|
93
93
|
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
94
94
|
kind: "reads",
|
|
95
|
+
readInBody: true,
|
|
95
96
|
summary: "Search the inbox by words and by meaning, ranked as one, with the same filters as the list.",
|
|
96
97
|
},
|
|
97
98
|
{
|
package/src/store/threads.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface ThreadFilters {
|
|
|
97
97
|
/**
|
|
98
98
|
* What a caller may filter the inbox by, and where to resume.
|
|
99
99
|
*
|
|
100
|
-
* **No free text.** Listing lists; `
|
|
100
|
+
* **No free text.** Listing lists; `POST {basePath}/threads/search` searches, ranked, with a limit and no
|
|
101
101
|
* cursor. A `q` here could say whether a thread matched, never how well.
|
|
102
102
|
*/
|
|
103
103
|
export interface ListThreadsQuery extends ThreadFilters {
|
package/src/version.generated.ts
CHANGED