@pithy-sh/support 0.5.1 → 0.6.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 +8 -0
- package/dist/audit/actions.d.ts +9 -0
- package/dist/audit/actions.d.ts.map +1 -1
- package/dist/audit/actions.js +9 -0
- package/dist/capability.d.ts +2 -1
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +30 -6
- package/dist/config/config.d.ts +63 -2
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +63 -5
- package/dist/data/assignment.d.ts +43 -0
- package/dist/data/assignment.d.ts.map +1 -0
- package/dist/data/assignment.js +38 -0
- package/dist/data/embedding.d.ts +21 -0
- package/dist/data/embedding.d.ts.map +1 -0
- package/dist/data/embedding.js +23 -0
- package/dist/data/tables.d.ts +28 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/tables.js +10 -2
- package/dist/data/thread.d.ts +9 -4
- package/dist/data/thread.d.ts.map +1 -1
- package/dist/data/thread.js +9 -4
- package/dist/http/guards.d.ts +32 -14
- package/dist/http/guards.d.ts.map +1 -1
- package/dist/http/guards.js +44 -16
- package/dist/http/handlers.d.ts +35 -2
- package/dist/http/handlers.d.ts.map +1 -1
- package/dist/http/handlers.js +81 -8
- package/dist/http/resolve.d.ts +8 -0
- package/dist/http/resolve.d.ts.map +1 -1
- package/dist/http/resolve.js +20 -1
- package/dist/http/responses.d.ts +355 -0
- package/dist/http/responses.d.ts.map +1 -1
- package/dist/http/responses.js +64 -2
- package/dist/http/routes.d.ts +25 -11
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +34 -5
- package/dist/http/schemas.d.ts +67 -3
- package/dist/http/schemas.d.ts.map +1 -1
- package/dist/http/schemas.js +44 -5
- package/dist/http/scopes.d.ts +10 -5
- package/dist/http/scopes.d.ts.map +1 -1
- package/dist/http/scopes.js +42 -6
- package/dist/http/views.d.ts +9 -1
- package/dist/http/views.d.ts.map +1 -1
- package/dist/http/views.js +40 -1
- package/dist/inbound/ingest.d.ts +1 -1
- package/dist/inbound/ingest.d.ts.map +1 -1
- package/dist/inbound/ingest.js +3 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/migrations/0002_assignment.d.ts +20 -0
- package/dist/migrations/0002_assignment.d.ts.map +1 -0
- package/dist/migrations/0002_assignment.js +45 -0
- package/dist/migrations/0003_embeddings.d.ts +15 -0
- package/dist/migrations/0003_embeddings.d.ts.map +1 -0
- package/dist/migrations/0003_embeddings.js +23 -0
- package/dist/migrations/set.d.ts +13 -0
- package/dist/migrations/set.d.ts.map +1 -0
- package/dist/migrations/set.js +21 -0
- package/dist/presence/room.d.ts +35 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +66 -0
- package/dist/provision/provisionSupport.d.ts +32 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -1
- package/dist/provision/provisionSupport.js +10 -2
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -1
- package/dist/provision/resolveSupportConfig.js +13 -2
- package/dist/purge/contribution.d.ts +49 -0
- package/dist/purge/contribution.d.ts.map +1 -0
- package/dist/purge/contribution.js +164 -0
- package/dist/secret/registry.d.ts +26 -2
- package/dist/secret/registry.d.ts.map +1 -1
- package/dist/secret/registry.js +15 -3
- package/dist/semantic/documents.d.ts +21 -0
- package/dist/semantic/documents.d.ts.map +1 -0
- package/dist/semantic/documents.js +128 -0
- package/dist/semantic/embed.d.ts +59 -0
- package/dist/semantic/embed.d.ts.map +1 -0
- package/dist/semantic/embed.js +59 -0
- package/dist/semantic/messageIndex.d.ts +46 -0
- package/dist/semantic/messageIndex.d.ts.map +1 -0
- package/dist/semantic/messageIndex.js +67 -0
- package/dist/semantic/query.d.ts +37 -0
- package/dist/semantic/query.d.ts.map +1 -0
- package/dist/semantic/query.js +46 -0
- package/dist/staff/staff.d.ts +22 -0
- package/dist/staff/staff.d.ts.map +1 -0
- package/dist/staff/staff.js +64 -0
- package/dist/store/assignment.d.ts +63 -0
- package/dist/store/assignment.d.ts.map +1 -0
- package/dist/store/assignment.js +86 -0
- package/dist/store/hybrid.d.ts +134 -0
- package/dist/store/hybrid.d.ts.map +1 -0
- package/dist/store/hybrid.js +185 -0
- package/dist/store/paging.d.ts +10 -0
- package/dist/store/paging.d.ts.map +1 -1
- package/dist/store/paging.js +11 -1
- package/dist/store/provisionIndex.d.ts +21 -0
- package/dist/store/provisionIndex.d.ts.map +1 -0
- package/dist/store/provisionIndex.js +36 -0
- package/dist/store/search.d.ts +18 -9
- package/dist/store/search.d.ts.map +1 -1
- package/dist/store/search.js +36 -34
- package/dist/store/searchIndex.d.ts +4 -4
- package/dist/store/searchIndex.js +4 -4
- package/dist/store/threads.d.ts +174 -8
- package/dist/store/threads.d.ts.map +1 -1
- package/dist/store/threads.js +33 -40
- package/dist/submission/submit.d.ts +1 -1
- package/dist/submission/submit.d.ts.map +1 -1
- package/dist/submission/submit.js +2 -1
- package/dist/test-utils/seed.d.ts +33 -0
- package/dist/test-utils/seed.d.ts.map +1 -0
- package/dist/test-utils/seed.js +102 -0
- package/dist/test-utils/semantic.d.ts +21 -0
- package/dist/test-utils/semantic.d.ts.map +1 -0
- package/dist/test-utils/semantic.js +107 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflows/specs.d.ts +23 -0
- package/dist/workflows/specs.d.ts.map +1 -1
- package/dist/workflows/specs.js +27 -7
- package/dist/workflows/worker.d.ts +15 -2
- package/dist/workflows/worker.d.ts.map +1 -1
- package/dist/workflows/worker.js +48 -4
- package/package.json +9 -7
- package/pithy.manifest.json +21 -3
- package/src/audit/actions.ts +9 -0
- package/src/capability.ts +43 -4
- package/src/config/config.ts +104 -5
- package/src/data/assignment.ts +55 -0
- package/src/data/embedding.ts +27 -0
- package/src/data/tables.ts +8 -0
- package/src/data/thread.ts +26 -4
- package/src/http/guards.ts +52 -15
- package/src/http/handlers.ts +125 -4
- package/src/http/resolve.ts +19 -0
- package/src/http/responses.ts +154 -0
- package/src/http/routes.ts +98 -12
- package/src/http/schemas.ts +117 -28
- package/src/http/scopes.ts +43 -6
- package/src/http/views.ts +44 -0
- package/src/inbound/ingest.ts +5 -2
- package/src/index.ts +5 -0
- package/src/migrations/0002_assignment.ts +65 -0
- package/src/migrations/0003_embeddings.ts +29 -0
- package/src/migrations/set.ts +21 -0
- package/src/presence/room.ts +94 -0
- package/src/provision/provisionSupport.ts +33 -1
- package/src/provision/resolveSupportConfig.ts +24 -13
- package/src/purge/contribution.ts +294 -0
- package/src/secret/registry.ts +17 -2
- package/src/semantic/documents.ts +178 -0
- package/src/semantic/embed.ts +99 -0
- package/src/semantic/messageIndex.ts +74 -0
- package/src/semantic/query.ts +66 -0
- package/src/staff/staff.ts +128 -0
- package/src/store/assignment.ts +179 -0
- package/src/store/hybrid.ts +314 -0
- package/src/store/paging.ts +14 -0
- package/src/store/provisionIndex.ts +46 -0
- package/src/store/search.ts +58 -37
- package/src/store/searchIndex.ts +4 -4
- package/src/store/threads.ts +91 -91
- package/src/submission/submit.ts +3 -2
- package/src/test-utils/seed.ts +141 -0
- package/src/test-utils/semantic.ts +110 -0
- package/src/version.generated.ts +1 -1
- package/src/workflows/specs.ts +34 -0
- package/src/workflows/worker.ts +84 -8
- package/src/workflows/wrangler.jsonc +8 -3
package/dist/http/routes.d.ts
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
4
4
|
import type { Context, Hono } from "hono";
|
|
5
|
+
import type { SupportStaffConfig } from "../config/config";
|
|
5
6
|
import { type HandlerDeps } from "./handlers";
|
|
6
7
|
/**
|
|
7
8
|
* The support routes, their verification strategies, and what each accepts:
|
|
8
9
|
*
|
|
9
10
|
* GET /support/threads → the inbox (control-plane: support:threads:read) query: ListThreadsQuery
|
|
11
|
+
* GET /support/threads/search → search (control-plane: support:threads:read) query: SearchThreadsQuery
|
|
10
12
|
* GET /support/threads/:id → one thread (control-plane: support:threads:read) param: ThreadIdParam
|
|
11
13
|
* POST /support/threads/:id/archive → done/reopen (control-plane: support:threads:archive) param + json
|
|
14
|
+
* POST /support/threads/:id/assign → who has it (control-plane: support:threads:assign) param + json
|
|
12
15
|
* POST /support/threads/:id/reply → answer (control-plane: support:threads:reply) param + json
|
|
13
16
|
* POST /support/threads/:id/reclassify→ re-run model (control-plane: support:threads:reclassify) param
|
|
14
17
|
* POST /support/threads/:id/flags → read/snooze (control-plane: support:threads:flag) param + json
|
|
@@ -18,20 +21,26 @@ import { type HandlerDeps } from "./handlers";
|
|
|
18
21
|
* GET /support/feedback → my requests (bearer/session) query: MyThreadsQuery
|
|
19
22
|
* GET /support/feedback/:id → one of mine (bearer/session) param: ThreadIdParam
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* POST /support/staff/threads/:id/assign → who has it (bearer/session + same-origin + staff assignPower) param + json
|
|
25
|
+
*
|
|
26
|
+
* **Three doors, three gates, and the control plane never stacks with a session.** The management routes
|
|
27
|
+
* answer to a control-plane credential the adopter issued, and with the seam uncomposed every one denies
|
|
28
|
+
* with `controlplane/not_connected` — the correct failure for an inbox holding other people's private
|
|
24
29
|
* correspondence. The `feedback` routes answer to the adopter's own signed-in user, and with no auth
|
|
25
|
-
* capability composed `c.var.auth` is null and every one denies.
|
|
26
|
-
*
|
|
30
|
+
* capability composed `c.var.auth` is null and every one denies. The `staff` route answers to a signed-in
|
|
31
|
+
* user too, and then to `requireStaff()`: a staff power from `support({ staff })`, held in the organization
|
|
32
|
+
* the request acts for, and with no `staff` configured every caller is refused. A control-plane caller
|
|
33
|
+
* holds no session by design and can never satisfy `requireAuth()`; a user's session confers no scope —
|
|
34
|
+
* which is why staff assignment is its own route rather than a second gate on the control-plane one. See
|
|
27
35
|
* `guards.ts`.
|
|
28
36
|
*
|
|
29
|
-
* **`requireSameOrigin()` is on the
|
|
30
|
-
* mutating route CSRF-reachable
|
|
31
|
-
* — a forged submission is somebody else's words attributed to them, in the one place an
|
|
32
|
-
* treats attribution as proven
|
|
33
|
-
*
|
|
34
|
-
*
|
|
37
|
+
* **`requireSameOrigin()` is on the two session routes that write, and on no read.** Cookie-mode sessions
|
|
38
|
+
* make a mutating route CSRF-reachable. The submission route writes into a support inbox under a real
|
|
39
|
+
* customer's name — a forged submission is somebody else's words attributed to them, in the one place an
|
|
40
|
+
* operator treats attribution as proven — and the staff assign route hands a customer's conversation to
|
|
41
|
+
* somebody on a staff member's say-so. The reads are GETs and carry no such risk. A bearer caller is
|
|
42
|
+
* CSRF-exempt, and that exemption belongs to the gate `@pithy-sh/auth` publishes rather than to anything
|
|
43
|
+
* decided here.
|
|
35
44
|
*
|
|
36
45
|
* **Every response has an exported schema**, in `responses.ts`. Each handler's return type is
|
|
37
46
|
* `z.output` of its envelope, so `c.json(await handler(...))` carries the contract without a cast — and
|
|
@@ -108,6 +117,11 @@ export interface SupportRoutesOptions {
|
|
|
108
117
|
submission?: boolean;
|
|
109
118
|
/** Resolve handler deps from the request context. */
|
|
110
119
|
resolveDeps: (c: Context<PithyHonoEnv>) => Promise<HandlerDeps>;
|
|
120
|
+
/**
|
|
121
|
+
* Who of the app's signed-in users works the inbox. Absent, the staff route is still mounted and refuses
|
|
122
|
+
* every caller with 403 — it is not a feature this deployment lacks, it is a door nobody holds a key to.
|
|
123
|
+
*/
|
|
124
|
+
staff?: SupportStaffConfig;
|
|
111
125
|
}
|
|
112
126
|
/** Register the support sub-router. Returned as the capability's `routes` hook. */
|
|
113
127
|
export declare function registerSupportRoutes(options: SupportRoutesOptions): (app: Hono<PithyHonoEnv>) => void;
|
|
@@ -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;
|
|
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,CAqNtG"}
|
package/dist/http/routes.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { requireAuth } from "./guards.js";
|
|
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";
|
|
6
|
-
import { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput, ThreadIdParam } from "./schemas.js";
|
|
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";
|
|
3
|
+
import { requireAuth, requireStaff } from "./guards.js";
|
|
4
|
+
import { archiveConversation, assignConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, searchInbox, submitFeedbackRequest, updateFlags } from "./handlers.js";
|
|
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, SearchThreadsQuery, SubmitFeedbackInput, ThreadIdParam } from "./schemas.js";
|
|
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";
|
|
10
10
|
import { describeRoute } from "@pithy-sh/core/src/http/routeSpec";
|
|
@@ -54,6 +54,12 @@ 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.get(`${base}/threads/search`, describeRoute({
|
|
58
|
+
summary: "Search the inbox, by words and by meaning, ranked as one.",
|
|
59
|
+
verification: "control-plane",
|
|
60
|
+
scopes: [SUPPORT_THREADS_READ_SCOPE],
|
|
61
|
+
responses: { 200: SupportSearchResponse }
|
|
62
|
+
}), requireControlPlane(SUPPORT_THREADS_READ_SCOPE), validate("query", SearchThreadsQuery), async (c) => c.json(await searchInbox(await resolve(c), c.req.valid("query"), viewer(c))));
|
|
57
63
|
app.get(`${base}/replies`, describeRoute({
|
|
58
64
|
summary: "List the canned replies.",
|
|
59
65
|
verification: "control-plane",
|
|
@@ -72,6 +78,23 @@ function registerSupportRoutes(options) {
|
|
|
72
78
|
scopes: [SUPPORT_THREADS_ARCHIVE_SCOPE],
|
|
73
79
|
responses: { 200: SupportArchiveResponse }
|
|
74
80
|
}), 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))));
|
|
81
|
+
app.post(`${base}/threads/:id/assign`, describeRoute({
|
|
82
|
+
summary: "Assign a conversation to somebody, or to nobody.",
|
|
83
|
+
verification: "control-plane",
|
|
84
|
+
scopes: [SUPPORT_THREADS_ASSIGN_SCOPE],
|
|
85
|
+
responses: { 200: SupportAssignResponse }
|
|
86
|
+
}), requireControlPlane(SUPPORT_THREADS_ASSIGN_SCOPE), validate("param", ThreadIdParam), validate("json", AssignThreadInput), async (c) => c.json(await assignConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), {
|
|
87
|
+
type: "control-plane",
|
|
88
|
+
id: viewer(c)
|
|
89
|
+
})));
|
|
90
|
+
app.post(`${base}/staff/threads/:id/assign`, describeRoute({
|
|
91
|
+
summary: "Assign a conversation as a signed-in member of the support staff.",
|
|
92
|
+
verification: ["bearer", "session"],
|
|
93
|
+
responses: { 200: SupportAssignResponse }
|
|
94
|
+
}), requireAuth(), requireSameOrigin(), requireStaff(options.staff, "assignPower"), validate("param", ThreadIdParam), validate("json", AssignThreadInput), async (c) => c.json(await assignConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), {
|
|
95
|
+
type: "user",
|
|
96
|
+
id: submitter(c)
|
|
97
|
+
})));
|
|
75
98
|
app.post(`${base}/threads/:id/reply`, describeRoute({
|
|
76
99
|
summary: "Answer the customer.",
|
|
77
100
|
verification: "control-plane",
|
|
@@ -93,16 +116,22 @@ function registerSupportRoutes(options) {
|
|
|
93
116
|
if (!submission) return;
|
|
94
117
|
app.post(`${base}/feedback`, describeRoute({
|
|
95
118
|
summary: "Write in to support from the app.",
|
|
119
|
+
operation: "feedback.write",
|
|
120
|
+
tool: { effect: "sends" },
|
|
96
121
|
verification: ["bearer", "session"],
|
|
97
122
|
responses: { 201: SupportSubmissionResponse }
|
|
98
123
|
}), requireAuth(), requireSameOrigin(), validate("json", SubmitFeedbackInput), async (c) => c.json(await submitFeedbackRequest(await resolve(c), c.req.valid("json"), submitter(c)), 201));
|
|
99
124
|
app.get(`${base}/feedback`, describeRoute({
|
|
100
125
|
summary: "List the caller's own support requests.",
|
|
126
|
+
operation: "feedback.list",
|
|
127
|
+
tool: true,
|
|
101
128
|
verification: ["bearer", "session"],
|
|
102
129
|
responses: { 200: SupportMyThreadsResponse }
|
|
103
130
|
}), requireAuth(), validate("query", MyThreadsQuery), async (c) => c.json(await listMyThreads(await resolve(c), c.req.valid("query"), submitter(c))));
|
|
104
131
|
app.get(`${base}/feedback/:id`, describeRoute({
|
|
105
132
|
summary: "Read one of the caller's own support requests.",
|
|
133
|
+
operation: "feedback.read",
|
|
134
|
+
tool: true,
|
|
106
135
|
verification: ["bearer", "session"],
|
|
107
136
|
responses: { 200: SupportMyThreadResponse }
|
|
108
137
|
}), requireAuth(), validate("param", ThreadIdParam), async (c) => c.json(await readMyThread(await resolve(c), c.req.valid("param").id, submitter(c))));
|
package/dist/http/schemas.d.ts
CHANGED
|
@@ -18,7 +18,13 @@ export declare const ThreadIdParam: z.ZodObject<{
|
|
|
18
18
|
id: z.ZodString;
|
|
19
19
|
}, z.core.$strip>;
|
|
20
20
|
export type ThreadIdParam = z.infer<typeof ThreadIdParam>;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The inbox listing query.
|
|
23
|
+
*
|
|
24
|
+
* **Strict, and there is no `q`.** Listing lists; `GET {basePath}/threads/search` searches. An unknown
|
|
25
|
+
* key is refused as `validation/invalid_input` rather than ignored, so a client sending `q` is told so
|
|
26
|
+
* instead of getting the unfiltered inbox back as though it were the answer.
|
|
27
|
+
*/
|
|
22
28
|
export declare const ListThreadsQuery: z.ZodObject<{
|
|
23
29
|
archived: z.ZodOptional<z.ZodEnum<{
|
|
24
30
|
false: "false";
|
|
@@ -42,16 +48,74 @@ export declare const ListThreadsQuery: z.ZodObject<{
|
|
|
42
48
|
email: "email";
|
|
43
49
|
}>>;
|
|
44
50
|
inbox: z.ZodOptional<z.ZodString>;
|
|
45
|
-
|
|
51
|
+
assignee: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodString]>>;
|
|
46
52
|
cursor: z.ZodOptional<z.ZodString>;
|
|
47
53
|
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
48
|
-
}, z.core.$
|
|
54
|
+
}, z.core.$strict>;
|
|
49
55
|
export type ListThreadsQuery = z.infer<typeof ListThreadsQuery>;
|
|
56
|
+
/** How a search runs. */
|
|
57
|
+
export declare const SupportSearchMode: z.ZodEnum<{
|
|
58
|
+
hybrid: "hybrid";
|
|
59
|
+
keyword: "keyword";
|
|
60
|
+
semantic: "semantic";
|
|
61
|
+
}>;
|
|
62
|
+
export type SupportSearchMode = z.infer<typeof SupportSearchMode>;
|
|
63
|
+
/** The search query. */
|
|
64
|
+
export declare const SearchThreadsQuery: z.ZodObject<{
|
|
65
|
+
archived: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
false: "false";
|
|
67
|
+
true: "true";
|
|
68
|
+
}>>;
|
|
69
|
+
category: z.ZodOptional<z.ZodString>;
|
|
70
|
+
declaredCategory: z.ZodOptional<z.ZodString>;
|
|
71
|
+
priority: z.ZodOptional<z.ZodEnum<{
|
|
72
|
+
low: "low";
|
|
73
|
+
normal: "normal";
|
|
74
|
+
urgent: "urgent";
|
|
75
|
+
}>>;
|
|
76
|
+
sentiment: z.ZodOptional<z.ZodEnum<{
|
|
77
|
+
angry: "angry";
|
|
78
|
+
frustrated: "frustrated";
|
|
79
|
+
neutral: "neutral";
|
|
80
|
+
positive: "positive";
|
|
81
|
+
}>>;
|
|
82
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
83
|
+
app: "app";
|
|
84
|
+
email: "email";
|
|
85
|
+
}>>;
|
|
86
|
+
inbox: z.ZodOptional<z.ZodString>;
|
|
87
|
+
assignee: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodString]>>;
|
|
88
|
+
q: z.ZodString;
|
|
89
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
90
|
+
hybrid: "hybrid";
|
|
91
|
+
keyword: "keyword";
|
|
92
|
+
semantic: "semantic";
|
|
93
|
+
}>>;
|
|
94
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
95
|
+
}, z.core.$strict>;
|
|
96
|
+
export type SearchThreadsQuery = z.infer<typeof SearchThreadsQuery>;
|
|
50
97
|
/** The archive/unarchive body. */
|
|
51
98
|
export declare const ArchiveThreadInput: z.ZodObject<{
|
|
52
99
|
archived: z.ZodBoolean;
|
|
53
100
|
}, z.core.$strip>;
|
|
54
101
|
export type ArchiveThreadInput = z.infer<typeof ArchiveThreadInput>;
|
|
102
|
+
/** Who a thread is assigned to. */
|
|
103
|
+
export declare const AssigneeInput: z.ZodObject<{
|
|
104
|
+
ref: z.ZodString;
|
|
105
|
+
name: z.ZodString;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
export type AssigneeInput = z.infer<typeof AssigneeInput>;
|
|
108
|
+
/** The longest assignment note, in UTF-16 code units. */
|
|
109
|
+
export declare const MAX_ASSIGNMENT_NOTE_CHARS = 500;
|
|
110
|
+
/** The assign/unassign body, the same on the control-plane route and the staff session route. */
|
|
111
|
+
export declare const AssignThreadInput: z.ZodObject<{
|
|
112
|
+
assignee: z.ZodNullable<z.ZodObject<{
|
|
113
|
+
ref: z.ZodString;
|
|
114
|
+
name: z.ZodString;
|
|
115
|
+
}, z.core.$strip>>;
|
|
116
|
+
note: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
export type AssignThreadInput = z.infer<typeof AssignThreadInput>;
|
|
55
119
|
/** The reply body. */
|
|
56
120
|
export declare const ReplyInput: z.ZodObject<{
|
|
57
121
|
body: z.ZodString;
|
|
@@ -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":"AAIA,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,wBAAwB;AACxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAuBqF,CAAC;AACrH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,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;;;iBAgB0B,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
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { MAX_SUBMISSION_BODY_CHARS } from "../config/config.js";
|
|
4
4
|
import { SupportChannel, SupportPriority, SupportSentiment } from "../data/enums.js";
|
|
5
|
+
import { UNASSIGNED } from "../data/assignment.js";
|
|
5
6
|
import { SupportSubmissionContext } from "../data/message.js";
|
|
6
7
|
import "../store/paging.js";
|
|
7
8
|
import { z } from "zod";
|
|
9
|
+
import { PresenceParticipantName, PresenceParticipantRef } from "@pithy-sh/core/src/presence/frames";
|
|
8
10
|
//#region src/http/schemas.ts
|
|
11
|
+
/** An assignee reference, as the listing filters on it and an assignment names it. */
|
|
12
|
+
const AssigneeRef = PresenceParticipantRef.refine((ref) => ref !== UNASSIGNED, { message: `"${UNASSIGNED}" is reserved: it is the filter for threads nobody has` }).describe("Who the thread is assigned to: an opaque reference, the participant shape presence rooms use. A management client's own reference, which it chose; or a user of the app's id. Never `none`, which is the listing's filter for threads nobody has.");
|
|
9
13
|
/**
|
|
10
14
|
* The request contracts every support route declares (CLAUDE.md §HTTP).
|
|
11
15
|
*
|
|
@@ -22,8 +26,8 @@ import { z } from "zod";
|
|
|
22
26
|
const ThreadIdParam = z.object({ id: z.string().uuid().describe("The thread's UUID. A param schema constrains the string; the handler still does the lookup.") }).describe("The path parameters of every single-thread route.");
|
|
23
27
|
/** A category key as a filter — bounded and shaped, never resolved against the configured set. */
|
|
24
28
|
const CategoryFilter = z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/).describe("One category key from this project's effective taxonomy. A key an adopter removed simply matches nothing.");
|
|
25
|
-
/** The inbox listing
|
|
26
|
-
const
|
|
29
|
+
/** The filters the inbox listing and the search route share, applied in D1 either way. */
|
|
30
|
+
const threadFilterFields = {
|
|
27
31
|
archived: z.enum(["true", "false"]).optional().describe("`true` for the done pile, `false` or absent for the open inbox — which is the default on purpose."),
|
|
28
32
|
category: CategoryFilter.optional().describe("Filter to one category — what the classifier made of it."),
|
|
29
33
|
declaredCategory: CategoryFilter.optional().describe("Filter to one category **the submitter chose**, which is a different question from `category` and answers it about different threads. Combining the two is how an operator finds the disagreements; asking for this one alone is how they triage a project with `ai.enabled: false`, where nothing ever writes the other."),
|
|
@@ -31,12 +35,47 @@ const ListThreadsQuery = z.object({
|
|
|
31
35
|
sentiment: SupportSentiment.optional().describe("Filter to one sentiment."),
|
|
32
36
|
channel: SupportChannel.optional().describe("Filter to one channel — `email` for mail, `app` for what signed-in users filed from inside the app. Absent shows both, which is the right default for an inbox that is one queue however things arrived."),
|
|
33
37
|
inbox: z.string().min(3).max(256).optional().describe("Filter to one configured inbox address, for a Worker serving several."),
|
|
34
|
-
|
|
38
|
+
assignee: z.union([z.literal(UNASSIGNED), AssigneeRef]).optional().describe("Filter to the threads one assignee has, by their `assigneeRef`, or `none` for the threads nobody has. Absent shows every thread, assigned or not.")
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The inbox listing query.
|
|
42
|
+
*
|
|
43
|
+
* **Strict, and there is no `q`.** Listing lists; `GET {basePath}/threads/search` searches. An unknown
|
|
44
|
+
* key is refused as `validation/invalid_input` rather than ignored, so a client sending `q` is told so
|
|
45
|
+
* instead of getting the unfiltered inbox back as though it were the answer.
|
|
46
|
+
*/
|
|
47
|
+
const ListThreadsQuery = z.object({
|
|
48
|
+
...threadFilterFields,
|
|
35
49
|
cursor: z.string().max(512).optional().describe("Where to resume, from the previous page's `nextCursor`. Opaque; a malformed one is a first page."),
|
|
36
50
|
limit: z.coerce.number().int().min(1).max(100).optional().describe("How many threads to return. Bounded, because a verified client can still have a bug.")
|
|
37
|
-
}).describe("The inbox query: what to filter by,
|
|
51
|
+
}).strict().describe("The inbox query: what to filter by, and where to resume. Search is its own route.");
|
|
52
|
+
/** How a search runs. */
|
|
53
|
+
const SupportSearchMode = z.enum([
|
|
54
|
+
"keyword",
|
|
55
|
+
"semantic",
|
|
56
|
+
"hybrid"
|
|
57
|
+
]).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
|
+
/** The search query. */
|
|
59
|
+
const SearchThreadsQuery = z.object({
|
|
60
|
+
q: 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
|
+
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.coerce.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.`),
|
|
63
|
+
...threadFilterFields
|
|
64
|
+
}).strict().describe("A search over the inbox: the words, the mode, how many, and the same filters the inbox listing takes.");
|
|
38
65
|
/** The archive/unarchive body. */
|
|
39
66
|
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.");
|
|
67
|
+
/** Who a thread is assigned to. */
|
|
68
|
+
const AssigneeInput = z.object({
|
|
69
|
+
ref: AssigneeRef,
|
|
70
|
+
name: PresenceParticipantName.describe("How the assignee is shown in the inbox and in the history. A label, never an identity — `ref` is that.")
|
|
71
|
+
}).describe("Somebody a thread may be assigned to: an opaque reference and the name to show.");
|
|
72
|
+
/** The longest assignment note, in UTF-16 code units. */
|
|
73
|
+
const MAX_ASSIGNMENT_NOTE_CHARS = 500;
|
|
74
|
+
/** The assign/unassign body, the same on the control-plane route and the staff session route. */
|
|
75
|
+
const AssignThreadInput = z.object({
|
|
76
|
+
assignee: AssigneeInput.nullable().describe("Who should have this conversation, or `null` to unassign it. Assigning the current assignee again changes nothing and records nothing."),
|
|
77
|
+
note: z.string().trim().min(1).max(500).optional().describe("Why, in your own words. Kept in the thread's assignment history and on the audit event, and shown to anybody who can read the thread — so it is a note to colleagues, never a place for the customer's details.")
|
|
78
|
+
}).describe("Assign a conversation to somebody, or to nobody.");
|
|
40
79
|
/** The reply body. */
|
|
41
80
|
const ReplyInput = z.object({
|
|
42
81
|
body: z.string().min(1).max(5e4).describe("The reply text, as a human wrote and edited it. Rendered HTML-escaped into the adopter's email shell — it is prose, never markup."),
|
|
@@ -88,4 +127,4 @@ const MyThreadsQuery = z.object({
|
|
|
88
127
|
/** The canned-reply catalog query. */
|
|
89
128
|
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.");
|
|
90
129
|
//#endregion
|
|
91
|
-
export { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput, SubmittedAttachmentInput, ThreadIdParam };
|
|
130
|
+
export { ArchiveThreadInput, AssignThreadInput, AssigneeInput, FlagsInput, ListThreadsQuery, MAX_ASSIGNMENT_NOTE_CHARS, MyThreadsQuery, RepliesQuery, ReplyInput, SearchThreadsQuery, SubmitFeedbackInput, SubmittedAttachmentInput, SupportSearchMode, ThreadIdParam };
|
package/dist/http/scopes.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import type {
|
|
3
|
+
import type { AdminRouteDeclaration } from "@pithy-sh/core/src/controlPlane/discovery/adminRoute";
|
|
4
4
|
import type { ControlPlaneScope } from "@pithy-sh/core/src/controlPlane/scope/scope";
|
|
5
5
|
/**
|
|
6
6
|
* Support's control-plane scopes, and the admin surface a manifest advertises.
|
|
7
7
|
*
|
|
8
|
-
* **Separate from `guards.ts` because a scope name is a client's business
|
|
8
|
+
* **Separate from `guards.ts` because a scope name is a client's business**. A management
|
|
9
9
|
* client reads these to render what a connection may do, and `pithy-sh/dashboard`'s scope builder
|
|
10
10
|
* writes the `pithy dashboard connect --scope …` command from exactly these constants — in a browser
|
|
11
11
|
* program, with the DOM lib and no Workers types. While they sat beside the Hono middleware, naming
|
|
@@ -13,7 +13,7 @@ import type { ControlPlaneScope } from "@pithy-sh/core/src/controlPlane/scope/sc
|
|
|
13
13
|
* program has none of. **This module imports types and nothing else, and a gate holds it there**:
|
|
14
14
|
* `tooling/browser-scopes` compiles a DOM-only program against every scope the kit declares.
|
|
15
15
|
*
|
|
16
|
-
* ##
|
|
16
|
+
* ## Six scopes, because these are six different blast radii
|
|
17
17
|
*
|
|
18
18
|
* The temptation is one `support:admin` flag. It is wrong on the merits, the same way payments'
|
|
19
19
|
* single flag was: **reading** an inbox exposes every customer's private correspondence, while
|
|
@@ -30,7 +30,7 @@ export declare const SUPPORT_THREADS_READ_SCOPE: ControlPlaneScope;
|
|
|
30
30
|
/** Mark a thread done or reopen it — the one shared piece of state in the model. */
|
|
31
31
|
export declare const SUPPORT_THREADS_ARCHIVE_SCOPE: ControlPlaneScope;
|
|
32
32
|
/**
|
|
33
|
-
* Send mail to a customer under the adopter's domain. The most dangerous of the
|
|
33
|
+
* Send mail to a customer under the adopter's domain. The most dangerous of the six and the reason
|
|
34
34
|
* a single admin flag was never an option.
|
|
35
35
|
*/
|
|
36
36
|
export declare const SUPPORT_THREADS_REPLY_SCOPE: ControlPlaneScope;
|
|
@@ -38,6 +38,11 @@ export declare const SUPPORT_THREADS_REPLY_SCOPE: ControlPlaneScope;
|
|
|
38
38
|
export declare const SUPPORT_THREADS_RECLASSIFY_SCOPE: ControlPlaneScope;
|
|
39
39
|
/** Set a viewer's own read/snooze flags. Private, uncoordinated, and the least dangerous thing here. */
|
|
40
40
|
export declare const SUPPORT_THREADS_FLAG_SCOPE: ControlPlaneScope;
|
|
41
|
+
/**
|
|
42
|
+
* Say who has a thread, or that nobody does. Shared state like archive, and its own scope like
|
|
43
|
+
* archive, because deciding who answers a customer is not the same permission as marking them done.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SUPPORT_THREADS_ASSIGN_SCOPE: ControlPlaneScope;
|
|
41
46
|
/**
|
|
42
47
|
* Every control-plane scope support defines — what `pithy dashboard connect` offers for this
|
|
43
48
|
* capability, and the list a manifest or a doc quotes rather than re-typing.
|
|
@@ -54,5 +59,5 @@ export declare const SUPPORT_CONTROL_PLANE_SCOPES: readonly ControlPlaneScope[];
|
|
|
54
59
|
* The summaries say what the operation is *for*. A client renders these next to a button somebody is
|
|
55
60
|
* about to press on a real customer's conversation.
|
|
56
61
|
*/
|
|
57
|
-
export declare function supportAdminRoutes(basePath: string):
|
|
62
|
+
export declare function supportAdminRoutes(basePath: string): AdminRouteDeclaration[];
|
|
58
63
|
//# sourceMappingURL=scopes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/http/scopes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,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,CA2E5E"}
|
package/dist/http/scopes.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Support's control-plane scopes, and the admin surface a manifest advertises.
|
|
6
6
|
*
|
|
7
|
-
* **Separate from `guards.ts` because a scope name is a client's business
|
|
7
|
+
* **Separate from `guards.ts` because a scope name is a client's business**. A management
|
|
8
8
|
* client reads these to render what a connection may do, and `pithy-sh/dashboard`'s scope builder
|
|
9
9
|
* writes the `pithy dashboard connect --scope …` command from exactly these constants — in a browser
|
|
10
10
|
* program, with the DOM lib and no Workers types. While they sat beside the Hono middleware, naming
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* program has none of. **This module imports types and nothing else, and a gate holds it there**:
|
|
13
13
|
* `tooling/browser-scopes` compiles a DOM-only program against every scope the kit declares.
|
|
14
14
|
*
|
|
15
|
-
* ##
|
|
15
|
+
* ## Six scopes, because these are six different blast radii
|
|
16
16
|
*
|
|
17
17
|
* The temptation is one `support:admin` flag. It is wrong on the merits, the same way payments'
|
|
18
18
|
* single flag was: **reading** an inbox exposes every customer's private correspondence, while
|
|
@@ -29,7 +29,7 @@ const SUPPORT_THREADS_READ_SCOPE = "support:threads:read";
|
|
|
29
29
|
/** Mark a thread done or reopen it — the one shared piece of state in the model. */
|
|
30
30
|
const SUPPORT_THREADS_ARCHIVE_SCOPE = "support:threads:archive";
|
|
31
31
|
/**
|
|
32
|
-
* Send mail to a customer under the adopter's domain. The most dangerous of the
|
|
32
|
+
* Send mail to a customer under the adopter's domain. The most dangerous of the six and the reason
|
|
33
33
|
* a single admin flag was never an option.
|
|
34
34
|
*/
|
|
35
35
|
const SUPPORT_THREADS_REPLY_SCOPE = "support:threads:reply";
|
|
@@ -38,6 +38,11 @@ const SUPPORT_THREADS_RECLASSIFY_SCOPE = "support:threads:reclassify";
|
|
|
38
38
|
/** Set a viewer's own read/snooze flags. Private, uncoordinated, and the least dangerous thing here. */
|
|
39
39
|
const SUPPORT_THREADS_FLAG_SCOPE = "support:threads:flag";
|
|
40
40
|
/**
|
|
41
|
+
* Say who has a thread, or that nobody does. Shared state like archive, and its own scope like
|
|
42
|
+
* archive, because deciding who answers a customer is not the same permission as marking them done.
|
|
43
|
+
*/
|
|
44
|
+
const SUPPORT_THREADS_ASSIGN_SCOPE = "support:threads:assign";
|
|
45
|
+
/**
|
|
41
46
|
* Every control-plane scope support defines — what `pithy dashboard connect` offers for this
|
|
42
47
|
* capability, and the list a manifest or a doc quotes rather than re-typing.
|
|
43
48
|
*/
|
|
@@ -46,7 +51,8 @@ const SUPPORT_CONTROL_PLANE_SCOPES = [
|
|
|
46
51
|
SUPPORT_THREADS_ARCHIVE_SCOPE,
|
|
47
52
|
SUPPORT_THREADS_REPLY_SCOPE,
|
|
48
53
|
SUPPORT_THREADS_RECLASSIFY_SCOPE,
|
|
49
|
-
SUPPORT_THREADS_FLAG_SCOPE
|
|
54
|
+
SUPPORT_THREADS_FLAG_SCOPE,
|
|
55
|
+
SUPPORT_THREADS_ASSIGN_SCOPE
|
|
50
56
|
];
|
|
51
57
|
/**
|
|
52
58
|
* Support's management surface, as `GET /control-plane/manifest` reports it.
|
|
@@ -63,47 +69,77 @@ function supportAdminRoutes(basePath) {
|
|
|
63
69
|
return [
|
|
64
70
|
{
|
|
65
71
|
method: "GET",
|
|
72
|
+
operation: "threads.list",
|
|
66
73
|
path: `${basePath}/threads`,
|
|
67
74
|
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
68
|
-
|
|
75
|
+
kind: "reads",
|
|
76
|
+
summary: "List the inbox, newest first, filtered by category, priority, assignee, and archived."
|
|
69
77
|
},
|
|
70
78
|
{
|
|
71
79
|
method: "GET",
|
|
80
|
+
operation: "threads.search",
|
|
81
|
+
path: `${basePath}/threads/search`,
|
|
82
|
+
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
83
|
+
kind: "reads",
|
|
84
|
+
summary: "Search the inbox by words and by meaning, ranked as one, with the same filters as the list."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
method: "GET",
|
|
88
|
+
operation: "threads.read",
|
|
72
89
|
path: `${basePath}/threads/:id`,
|
|
73
90
|
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
91
|
+
kind: "reads",
|
|
74
92
|
summary: "Read one conversation, with the sender's linked account and what they bought."
|
|
75
93
|
},
|
|
76
94
|
{
|
|
77
95
|
method: "POST",
|
|
96
|
+
operation: "threads.archive",
|
|
78
97
|
path: `${basePath}/threads/:id/archive`,
|
|
79
98
|
scope: SUPPORT_THREADS_ARCHIVE_SCOPE,
|
|
99
|
+
kind: "writes",
|
|
80
100
|
summary: "Mark a conversation done, or reopen one."
|
|
81
101
|
},
|
|
82
102
|
{
|
|
83
103
|
method: "POST",
|
|
104
|
+
operation: "threads.assign",
|
|
105
|
+
path: `${basePath}/threads/:id/assign`,
|
|
106
|
+
scope: SUPPORT_THREADS_ASSIGN_SCOPE,
|
|
107
|
+
kind: "writes",
|
|
108
|
+
summary: "Give a conversation to somebody, hand it on, or give it up."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
operation: "threads.reply",
|
|
84
113
|
path: `${basePath}/threads/:id/reply`,
|
|
85
114
|
scope: SUPPORT_THREADS_REPLY_SCOPE,
|
|
115
|
+
kind: "sends",
|
|
86
116
|
summary: "Answer the customer, threaded so their mail client keeps one conversation."
|
|
87
117
|
},
|
|
88
118
|
{
|
|
89
119
|
method: "POST",
|
|
120
|
+
operation: "threads.reclassify",
|
|
90
121
|
path: `${basePath}/threads/:id/reclassify`,
|
|
91
122
|
scope: SUPPORT_THREADS_RECLASSIFY_SCOPE,
|
|
123
|
+
kind: "writes",
|
|
92
124
|
summary: "Re-run the classifier over this conversation's latest inbound message."
|
|
93
125
|
},
|
|
94
126
|
{
|
|
95
127
|
method: "POST",
|
|
128
|
+
operation: "threads.flags",
|
|
96
129
|
path: `${basePath}/threads/:id/flags`,
|
|
97
130
|
scope: SUPPORT_THREADS_FLAG_SCOPE,
|
|
131
|
+
kind: "writes",
|
|
98
132
|
summary: "Set your own read and snooze state for a conversation. Private to you."
|
|
99
133
|
},
|
|
100
134
|
{
|
|
101
135
|
method: "GET",
|
|
136
|
+
operation: "replies.list",
|
|
102
137
|
path: `${basePath}/replies`,
|
|
103
138
|
scope: SUPPORT_THREADS_READ_SCOPE,
|
|
139
|
+
kind: "reads",
|
|
104
140
|
summary: "The canned reply catalog — starting points to pick from, ordered for a thread's category."
|
|
105
141
|
}
|
|
106
142
|
];
|
|
107
143
|
}
|
|
108
144
|
//#endregion
|
|
109
|
-
export { SUPPORT_CONTROL_PLANE_SCOPES, SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE, supportAdminRoutes };
|
|
145
|
+
export { SUPPORT_CONTROL_PLANE_SCOPES, SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_ASSIGN_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE, supportAdminRoutes };
|
package/dist/http/views.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { SupportAssignment } from "../data/assignment";
|
|
3
4
|
import type { SupportMessage } from "../data/message";
|
|
4
5
|
import type { SupportThread } from "../data/thread";
|
|
5
6
|
import type { SenderContext } from "../link/sender";
|
|
7
|
+
import type { SearchOutcome, SearchResult } from "../store/hybrid";
|
|
6
8
|
import type { ListedThread } from "../store/threads";
|
|
7
|
-
import type { SenderContextView, SupportAttachmentView, SupportListedThreadView, SupportMessageView, SupportMyMessageView, SupportMyThreadView, SupportThreadView } from "./responses";
|
|
9
|
+
import type { SenderContextView, SupportAssignmentView, SupportAttachmentView, SupportListedThreadView, SupportMessageView, SupportMyMessageView, SupportMyThreadView, SupportSearchResponse, SupportSearchResultView, SupportThreadView } from "./responses";
|
|
8
10
|
/** Project one thread row. */
|
|
9
11
|
export declare function threadView(thread: SupportThread): SupportThreadView;
|
|
12
|
+
/** Project one change of a thread's assignee. The thread id is the route's, so it is not repeated. */
|
|
13
|
+
export declare function assignmentView(assignment: SupportAssignment): SupportAssignmentView;
|
|
10
14
|
/** Project one thread for the inbox, carrying this viewer's own read and snooze state. */
|
|
11
15
|
export declare function listedThreadView(thread: ListedThread): SupportListedThreadView;
|
|
16
|
+
/** Project one search result: the thread as this viewer sees it, and its best message's raw scores. */
|
|
17
|
+
export declare function searchResultView(result: SearchResult): SupportSearchResultView;
|
|
18
|
+
/** Project a whole search. */
|
|
19
|
+
export declare function searchView(outcome: SearchOutcome): SupportSearchResponse;
|
|
12
20
|
/**
|
|
13
21
|
* Project one message.
|
|
14
22
|
*
|
package/dist/http/views.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../../src/http/views.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAyBrB,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../../src/http/views.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAyBrB,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CA+BnE;AAED,sGAAsG;AACtG,wBAAgB,cAAc,CAAC,UAAU,EAAE,iBAAiB,GAAG,qBAAqB,CAYnF;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,uBAAuB,CAE9E;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,uBAAuB,CAO9E;AAED,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,qBAAqB,CAOxE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,kBAAkB,CAgBvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,mBAAmB,CASvE;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,qBAAqB,EAAE,GAAG,oBAAoB,CAYjH;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CA4BnE"}
|
package/dist/http/views.js
CHANGED
|
@@ -44,6 +44,10 @@ function threadView(thread) {
|
|
|
44
44
|
archived: thread.archived,
|
|
45
45
|
archivedAt: iso(thread.archivedAt),
|
|
46
46
|
archivedBy: thread.archivedBy ?? null,
|
|
47
|
+
assigneeRef: thread.assigneeRef ?? null,
|
|
48
|
+
assigneeName: thread.assigneeName ?? null,
|
|
49
|
+
assignedAt: iso(thread.assignedAt),
|
|
50
|
+
assignedBy: thread.assignedBy ?? null,
|
|
47
51
|
messageCount: thread.messageCount,
|
|
48
52
|
firstMessageAt: thread.firstMessageAt.toISOString(),
|
|
49
53
|
lastMessageAt: thread.lastMessageAt.toISOString(),
|
|
@@ -51,6 +55,20 @@ function threadView(thread) {
|
|
|
51
55
|
updatedAt: thread.updatedAt.toISOString()
|
|
52
56
|
};
|
|
53
57
|
}
|
|
58
|
+
/** Project one change of a thread's assignee. The thread id is the route's, so it is not repeated. */
|
|
59
|
+
function assignmentView(assignment) {
|
|
60
|
+
return {
|
|
61
|
+
id: assignment.id,
|
|
62
|
+
assigneeRef: assignment.assigneeRef ?? null,
|
|
63
|
+
assigneeName: assignment.assigneeName ?? null,
|
|
64
|
+
previousRef: assignment.previousRef ?? null,
|
|
65
|
+
previousName: assignment.previousName ?? null,
|
|
66
|
+
actorType: assignment.actorType,
|
|
67
|
+
assignedBy: assignment.assignedBy,
|
|
68
|
+
note: assignment.note ?? null,
|
|
69
|
+
createdAt: assignment.createdAt.toISOString()
|
|
70
|
+
};
|
|
71
|
+
}
|
|
54
72
|
/** Project one thread for the inbox, carrying this viewer's own read and snooze state. */
|
|
55
73
|
function listedThreadView(thread) {
|
|
56
74
|
return {
|
|
@@ -59,6 +77,27 @@ function listedThreadView(thread) {
|
|
|
59
77
|
snoozedUntil: iso(thread.snoozedUntil)
|
|
60
78
|
};
|
|
61
79
|
}
|
|
80
|
+
/** Project one search result: the thread as this viewer sees it, and its best message's raw scores. */
|
|
81
|
+
function searchResultView(result) {
|
|
82
|
+
return {
|
|
83
|
+
thread: listedThreadView(result.thread),
|
|
84
|
+
messageId: result.messageId,
|
|
85
|
+
...result.bm25 !== void 0 ? { bm25: result.bm25 } : {},
|
|
86
|
+
...result.similarity !== void 0 ? { similarity: result.similarity } : {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Project a whole search. */
|
|
90
|
+
function searchView(outcome) {
|
|
91
|
+
return {
|
|
92
|
+
mode: outcome.mode,
|
|
93
|
+
backends: {
|
|
94
|
+
ran: [...outcome.ran],
|
|
95
|
+
skipped: outcome.skipped.map((skip) => ({ ...skip }))
|
|
96
|
+
},
|
|
97
|
+
count: outcome.results.length,
|
|
98
|
+
results: outcome.results.map(searchResultView)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
62
101
|
/**
|
|
63
102
|
* Project one message.
|
|
64
103
|
*
|
|
@@ -143,4 +182,4 @@ function senderView(sender) {
|
|
|
143
182
|
};
|
|
144
183
|
}
|
|
145
184
|
//#endregion
|
|
146
|
-
export { listedThreadView, messageView, myMessageView, myThreadView, senderView, threadView };
|
|
185
|
+
export { assignmentView, listedThreadView, messageView, myMessageView, myThreadView, searchResultView, searchView, senderView, threadView };
|
package/dist/inbound/ingest.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { R2Bucket } from "@cloudflare/workers-types";
|
|
4
4
|
import type { AuditEmit } from "@pithy-sh/core/src/audit/recorder";
|
|
5
5
|
import type { Logger } from "@pithy-sh/core/src/logger/logger";
|
|
6
|
-
import type
|
|
6
|
+
import { type SupportConfig } from "../config/config";
|
|
7
7
|
import { type SupportDatabase } from "../data/tables";
|
|
8
8
|
/**
|
|
9
9
|
* Ingest — everything the `email()` handler does, with every dependency injected.
|