@pithy-sh/support 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/classify.d.ts +92 -0
- package/dist/ai/classify.d.ts.map +1 -0
- package/dist/ai/classify.js +153 -0
- package/dist/attachment/store.d.ts +55 -0
- package/dist/attachment/store.d.ts.map +1 -0
- package/dist/attachment/store.js +69 -0
- package/dist/audit/actions.d.ts +70 -0
- package/dist/audit/actions.d.ts.map +1 -0
- package/dist/audit/actions.js +70 -0
- package/dist/capability.d.ts +95 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +189 -0
- package/dist/client/projection.d.ts +58 -0
- package/dist/client/projection.d.ts.map +1 -0
- package/dist/client/projection.js +2 -0
- package/dist/config/config.d.ts +217 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +171 -0
- package/dist/data/attachment.d.ts +32 -0
- package/dist/data/attachment.d.ts.map +1 -0
- package/dist/data/attachment.js +34 -0
- package/dist/data/billingScope.d.ts +32 -0
- package/dist/data/billingScope.d.ts.map +1 -0
- package/dist/data/billingScope.js +34 -0
- package/dist/data/categories.d.ts +56 -0
- package/dist/data/categories.d.ts.map +1 -0
- package/dist/data/categories.js +99 -0
- package/dist/data/classification.d.ts +36 -0
- package/dist/data/classification.d.ts.map +1 -0
- package/dist/data/classification.js +30 -0
- package/dist/data/enums.d.ts +84 -0
- package/dist/data/enums.d.ts.map +1 -0
- package/dist/data/enums.js +72 -0
- package/dist/data/flag.d.ts +27 -0
- package/dist/data/flag.d.ts.map +1 -0
- package/dist/data/flag.js +29 -0
- package/dist/data/message.d.ts +83 -0
- package/dist/data/message.d.ts.map +1 -0
- package/dist/data/message.js +86 -0
- package/dist/data/tables.d.ts +162 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tables.js +46 -0
- package/dist/data/thread.d.ts +79 -0
- package/dist/data/thread.d.ts.map +1 -0
- package/dist/data/thread.js +67 -0
- package/dist/error/errors.d.ts +66 -0
- package/dist/error/errors.d.ts.map +1 -0
- package/dist/error/errors.js +84 -0
- package/dist/http/guards.d.ts +46 -0
- package/dist/http/guards.d.ts.map +1 -0
- package/dist/http/guards.js +55 -0
- package/dist/http/handlers.d.ts +91 -0
- package/dist/http/handlers.d.ts.map +1 -0
- package/dist/http/handlers.js +250 -0
- package/dist/http/resolve.d.ts +46 -0
- package/dist/http/resolve.d.ts.map +1 -0
- package/dist/http/resolve.js +74 -0
- package/dist/http/responses.d.ts +831 -0
- package/dist/http/responses.d.ts.map +1 -0
- package/dist/http/responses.js +305 -0
- package/dist/http/routes.d.ts +111 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +64 -0
- package/dist/http/schemas.d.ts +123 -0
- package/dist/http/schemas.d.ts.map +1 -0
- package/dist/http/schemas.js +91 -0
- package/dist/http/scopes.d.ts +58 -0
- package/dist/http/scopes.d.ts.map +1 -0
- package/dist/http/scopes.js +109 -0
- package/dist/http/views.d.ts +38 -0
- package/dist/http/views.d.ts.map +1 -0
- package/dist/http/views.js +146 -0
- package/dist/inbound/authenticity.d.ts +83 -0
- package/dist/inbound/authenticity.d.ts.map +1 -0
- package/dist/inbound/authenticity.js +63 -0
- package/dist/inbound/guard.d.ts +76 -0
- package/dist/inbound/guard.d.ts.map +1 -0
- package/dist/inbound/guard.js +80 -0
- package/dist/inbound/handler.d.ts +30 -0
- package/dist/inbound/handler.d.ts.map +1 -0
- package/dist/inbound/handler.js +84 -0
- package/dist/inbound/ingest.d.ts +106 -0
- package/dist/inbound/ingest.d.ts.map +1 -0
- package/dist/inbound/ingest.js +328 -0
- package/dist/inbound/recipient.d.ts +18 -0
- package/dist/inbound/recipient.d.ts.map +1 -0
- package/dist/inbound/recipient.js +48 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/link/sender.d.ts +162 -0
- package/dist/link/sender.d.ts.map +1 -0
- package/dist/link/sender.js +202 -0
- package/dist/migrations/0001_threads.d.ts +28 -0
- package/dist/migrations/0001_threads.d.ts.map +1 -0
- package/dist/migrations/0001_threads.js +105 -0
- package/dist/mime/address.d.ts +24 -0
- package/dist/mime/address.d.ts.map +1 -0
- package/dist/mime/address.js +30 -0
- package/dist/mime/parse.d.ts +98 -0
- package/dist/mime/parse.d.ts.map +1 -0
- package/dist/mime/parse.js +148 -0
- package/dist/mime/sanitize.d.ts +27 -0
- package/dist/mime/sanitize.d.ts.map +1 -0
- package/dist/mime/sanitize.js +214 -0
- package/dist/mime/threading.d.ts +81 -0
- package/dist/mime/threading.d.ts.map +1 -0
- package/dist/mime/threading.js +113 -0
- package/dist/mime/truncate.d.ts +19 -0
- package/dist/mime/truncate.d.ts.map +1 -0
- package/dist/mime/truncate.js +49 -0
- package/dist/provision/provisionSupport.d.ts +160 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -0
- package/dist/provision/provisionSupport.js +112 -0
- package/dist/provision/resolveSupportConfig.d.ts +47 -0
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -0
- package/dist/provision/resolveSupportConfig.js +24 -0
- package/dist/reply/send.d.ts +125 -0
- package/dist/reply/send.d.ts.map +1 -0
- package/dist/reply/send.js +155 -0
- package/dist/reply/snippets.d.ts +81 -0
- package/dist/reply/snippets.d.ts.map +1 -0
- package/dist/reply/snippets.js +137 -0
- package/dist/secret/registry.d.ts +20 -0
- package/dist/secret/registry.d.ts.map +1 -0
- package/dist/secret/registry.js +23 -0
- package/dist/seeds/example.d.ts +5 -0
- package/dist/seeds/example.d.ts.map +1 -0
- package/dist/seeds/example.js +352 -0
- package/dist/store/paging.d.ts +20 -0
- package/dist/store/paging.d.ts.map +1 -0
- package/dist/store/paging.js +22 -0
- package/dist/store/search.d.ts +69 -0
- package/dist/store/search.d.ts.map +1 -0
- package/dist/store/search.js +162 -0
- package/dist/store/searchIndex.d.ts +55 -0
- package/dist/store/searchIndex.d.ts.map +1 -0
- package/dist/store/searchIndex.js +68 -0
- package/dist/store/threads.d.ts +149 -0
- package/dist/store/threads.d.ts.map +1 -0
- package/dist/store/threads.js +199 -0
- package/dist/submission/encoding.d.ts +23 -0
- package/dist/submission/encoding.d.ts.map +1 -0
- package/dist/submission/encoding.js +57 -0
- package/dist/submission/guard.d.ts +73 -0
- package/dist/submission/guard.d.ts.map +1 -0
- package/dist/submission/guard.js +48 -0
- package/dist/submission/submit.d.ts +154 -0
- package/dist/submission/submit.d.ts.map +1 -0
- package/dist/submission/submit.js +321 -0
- package/dist/version.generated.d.ts +7 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +9 -0
- package/dist/workflows/classify.d.ts +65 -0
- package/dist/workflows/classify.d.ts.map +1 -0
- package/dist/workflows/classify.js +70 -0
- package/dist/workflows/retryPolicy.d.ts +41 -0
- package/dist/workflows/retryPolicy.d.ts.map +1 -0
- package/dist/workflows/retryPolicy.js +45 -0
- package/dist/workflows/specs.d.ts +45 -0
- package/dist/workflows/specs.d.ts.map +1 -0
- package/dist/workflows/specs.js +48 -0
- package/dist/workflows/worker.d.ts +50 -0
- package/dist/workflows/worker.d.ts.map +1 -0
- package/dist/workflows/worker.js +39 -0
- package/package.json +24 -15
- package/src/http/handlers.ts +1 -1
- package/src/submission/encoding.ts +1 -1
- package/src/submission/submit.ts +1 -1
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/http/responses.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkDxB,mEAAmE;AACnE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8D0C,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;;;;;;;;iBAeuE,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;AAE7E,gCAAgC;AAChC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQkE,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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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,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;;;;;;;;;;;;;;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"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { SupportAccountLinkSource, SupportChannel, SupportMessageDirection, SupportPriority, SupportSentiment } from "../data/enums.js";
|
|
4
|
+
import { SupportSubmissionContext } from "../data/message.js";
|
|
5
|
+
import { SUPPORT_BILLING_SCOPE } from "../data/billingScope.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { asRead } from "@pithy-sh/core/src/projection/asRead";
|
|
8
|
+
//#region src/http/responses.ts
|
|
9
|
+
/**
|
|
10
|
+
* What the support routes return, as Zod objects a management client can validate against.
|
|
11
|
+
*
|
|
12
|
+
* `schemas.ts` bounds what a caller may send; this file states what it gets back. Both halves are
|
|
13
|
+
* runtime values for the same reason: a management client reading a customer's Worker is crossing a
|
|
14
|
+
* trust boundary and must validate what comes back, and a TypeScript interface is erased before it
|
|
15
|
+
* can help — so every client that had only an interface hand-wrote a mirror, and the mirror drifted
|
|
16
|
+
* the first time a field landed here.
|
|
17
|
+
*
|
|
18
|
+
* **Dates are ISO-8601 strings, and that is not a change.** These handlers used to hand `Date` objects
|
|
19
|
+
* to `c.json`, which serializes them as ISO strings anyway — so the bytes on the wire were always
|
|
20
|
+
* this, while the type said otherwise and no schema could describe it. `views.ts` now converts
|
|
21
|
+
* explicitly, which is what makes the shape statable at all.
|
|
22
|
+
*
|
|
23
|
+
* **No codecs, and no transform anywhere in this file.** These describe JSON on the wire, so parsing
|
|
24
|
+
* one hands back exactly what went in — which is what lets `responses.test.ts` compare a parsed value
|
|
25
|
+
* with the projection's output and fail on a field either side forgot.
|
|
26
|
+
*
|
|
27
|
+
* **A browser imports this file, so it reaches no module that needs the Workers runtime.** That is not
|
|
28
|
+
* a preference about bundle size; it is what "a client validates what comes back" means once the client
|
|
29
|
+
* is a Vite program with the DOM lib and no Workers types. This file imported `SUPPORT_BILLING_SCOPE`
|
|
30
|
+
* from `link/sender.ts` for one release and brought D1, Kysely and two capabilities' table maps with it,
|
|
31
|
+
* which took `pithy-sh/dashboard`'s client build down on an error in `@pithy-sh/auth` (#419). The rule is
|
|
32
|
+
* held by `tooling/browser-scopes`, which compiles this module with `types: []` and asks the compiler
|
|
33
|
+
* which files that program included.
|
|
34
|
+
*
|
|
35
|
+
* **A field added here later is `.optional()`, not merely `.nullable()`.** This module is read across a
|
|
36
|
+
* version boundary — a management client validates a response with this schema against a customer's
|
|
37
|
+
* Worker at whatever kit version it is on — so an additive required key fails `safeParse` for everyone
|
|
38
|
+
* below that release and takes the whole pane with it (#450). Absent then means *this Worker cannot
|
|
39
|
+
* say*, which is a different fact from `null`.
|
|
40
|
+
*/
|
|
41
|
+
/** Where a page resumes, or the end of the list. */
|
|
42
|
+
const NextCursor = z.string().nullable().describe("Where the next page resumes. Null at the end of the list. Opaque — pass it back verbatim.");
|
|
43
|
+
/** One conversation, as the inbox and the reading pane show it. */
|
|
44
|
+
const SupportThreadView = z.object({
|
|
45
|
+
id: z.string().describe("The thread's UUID — what every other route on this surface takes."),
|
|
46
|
+
channel: SupportChannel.describe("How this conversation started — `email` at an inbound address, or `app` from a signed-in user of the adopter's own app."),
|
|
47
|
+
inboxAddress: z.string().nullable().describe("The support address this thread arrived on, lowercased, and the address a reply comes back to. Null only on an `app` thread in a project with no inbound address configured."),
|
|
48
|
+
subject: z.string().describe("The subject of the message that opened the thread. Later replies never rewrite it."),
|
|
49
|
+
fromAddress: z.string().describe("The sender's address, lowercased."),
|
|
50
|
+
fromName: z.string().nullable().describe("The sender's display name, or null. Untrusted text; render it escaped."),
|
|
51
|
+
senderAuthenticated: z.boolean().describe("Whether the sender was proved to be who they claim. On `email` that means the `From:` header was proved, and false means unproven rather than forged — it is what withholds the customer link. Always true on `app`, where a session proved it before the request arrived."),
|
|
52
|
+
userId: z.string().nullable().describe("The user this sender resolves to, or null when nobody or unproven."),
|
|
53
|
+
accountLinkSource: SupportAccountLinkSource.nullable().describe("How `userId` was established, or null when there is no link. **Render these differently.** `session` means an authenticated request proved it and the account *is* the caller; `email_address` means it was matched against an address in a header nobody proved. The same operator action — a refund, a reset — follows from very different evidence."),
|
|
54
|
+
declaredCategory: z.string().nullable().describe("**What the submitter said this is about**, or null when nobody said — every mail thread, and every app thread filed without a chooser. **Render this and `category` differently, and never fold one into the other.** They are two facts with two authors: this is a claim by the person writing, `category` is a machine's judgment about them, and an operator deciding what to do needs to know which is on screen. Where they disagree, that disagreement is the most useful thing on the row. Where `classifiedAt` is null this is the only category anybody stated."),
|
|
55
|
+
category: z.string().describe("**What the classifier made of it** — the current category key, from this project's own federated taxonomy. `uncategorized` until a classification lands, and forever on a project with `ai.enabled: false`."),
|
|
56
|
+
priority: SupportPriority.describe("How fast this thread needs a human."),
|
|
57
|
+
sentiment: SupportSentiment.describe("How the sender sounds — the churn signal."),
|
|
58
|
+
confidence: z.number().min(0).max(1).nullable().describe("The model's confidence in the current classification, or null until one lands."),
|
|
59
|
+
model: z.string().nullable().describe("The Workers AI model that produced the classification, or null."),
|
|
60
|
+
classifiedAt: z.iso.datetime().nullable().describe("When the classification was written, ISO-8601; null until it is."),
|
|
61
|
+
archived: z.boolean().describe("Done. One shared boolean, not per viewer."),
|
|
62
|
+
archivedAt: z.iso.datetime().nullable().describe("When it was last archived, ISO-8601; null while open."),
|
|
63
|
+
archivedBy: z.string().nullable().describe("The control-plane subject that archived it, for convenience only — the answer to 'who marked this done' comes from the audit trail."),
|
|
64
|
+
messageCount: z.number().int().describe("How many messages the thread holds, inbound and outbound together."),
|
|
65
|
+
firstMessageAt: z.iso.datetime().describe("When the thread opened, ISO-8601."),
|
|
66
|
+
lastMessageAt: z.iso.datetime().describe("When the thread last moved, ISO-8601. The inbox sorts on this."),
|
|
67
|
+
createdAt: z.iso.datetime().describe("When the thread row was created, ISO-8601."),
|
|
68
|
+
updatedAt: z.iso.datetime().describe("When the thread row was last written, ISO-8601.")
|
|
69
|
+
}).describe("One support conversation, as a management client sees it.");
|
|
70
|
+
/**
|
|
71
|
+
* A thread in the inbox listing — the thread plus this viewer's own private state.
|
|
72
|
+
*
|
|
73
|
+
* Re-described after `.extend()`: extending builds a new schema, and a description does not follow it.
|
|
74
|
+
*/
|
|
75
|
+
const SupportListedThreadView = SupportThreadView.extend({
|
|
76
|
+
read: z.boolean().describe("Whether this viewer has read it. False when they have no flag row, the common case."),
|
|
77
|
+
snoozedUntil: z.iso.datetime().nullable().describe("When this viewer's snooze expires, ISO-8601, or null.")
|
|
78
|
+
}).describe("One conversation in the inbox, carrying the calling viewer's own read and snooze state.");
|
|
79
|
+
/** One message in a conversation. */
|
|
80
|
+
const SupportMessageView = z.object({
|
|
81
|
+
id: z.string().describe("The message's UUID — Pithy's id, not the sender's."),
|
|
82
|
+
direction: SupportMessageDirection.describe("`inbound` from the customer, `outbound` from this Worker."),
|
|
83
|
+
channel: SupportChannel.describe("How this message traveled, which on an outbound row says how the answer was delivered: `email` means it went out, `app` means it is waiting for the submitter to read it and no mail was sent."),
|
|
84
|
+
context: SupportSubmissionContext.nullable().describe("The bounded context an app submission carried — screen, build, platform, environment, locale. Null on every mail-path message."),
|
|
85
|
+
fromAddress: z.string().nullable().describe("The address this message was sent from, lowercased. Null on an answer delivered in the app, which left no envelope."),
|
|
86
|
+
fromName: z.string().nullable().describe("The sender's display name, or null. Untrusted text."),
|
|
87
|
+
toAddress: z.string().nullable().describe("The address this message was addressed to, lowercased. Null on an app submission with no envelope."),
|
|
88
|
+
subject: z.string().describe("This message's own subject, which may differ from the thread's."),
|
|
89
|
+
textBody: z.string().describe("The plain-text body."),
|
|
90
|
+
htmlBody: z.string().nullable().describe("The sanitized HTML body, or null. Sanitized at ingest; the raw original stays in R2 and is never served."),
|
|
91
|
+
emailJobId: z.string().nullable().describe("The email job this message was enqueued as. Present exactly when `direction` is `outbound` and `channel` is `email`; read `channel` to ask whether an answer went out, never the absence of this."),
|
|
92
|
+
receivedAt: z.iso.datetime().describe("When the message arrived or was sent, ISO-8601.")
|
|
93
|
+
}).describe("One message in a conversation, with the threading internals dropped.");
|
|
94
|
+
/** One attachment — metadata plus a short-lived URL, never the storage key. */
|
|
95
|
+
const SupportAttachmentView = z.object({
|
|
96
|
+
id: z.string().describe("The attachment's UUID."),
|
|
97
|
+
filename: z.string().describe("The filename as it arrived, sanitized."),
|
|
98
|
+
contentType: z.string().describe("The declared content type."),
|
|
99
|
+
size: z.number().int().describe("The size in bytes."),
|
|
100
|
+
sha256: z.string().describe("The content digest, so a client can tell two identical attachments apart from one."),
|
|
101
|
+
inline: z.boolean().describe("Whether it was referenced inline in the HTML body rather than attached."),
|
|
102
|
+
url: z.string().nullable().describe("A signed URL valid for a few minutes, or null when no credentials were available to sign one. The `storageKey` is never here: it is server-derived precisely so a client cannot name an object or guess the one beside it.")
|
|
103
|
+
}).describe("One attachment as a client receives it — metadata and a signed URL, never the storage key.");
|
|
104
|
+
/** One of the sender's purchases, flattened to what a support console renders. */
|
|
105
|
+
const SenderPurchaseView = z.object({
|
|
106
|
+
id: z.string().describe("The purchase row id."),
|
|
107
|
+
rail: z.string().describe("Which rail it went through — `apple`, `google`, or `stripe`."),
|
|
108
|
+
productId: z.string().describe("The Pithy product key."),
|
|
109
|
+
status: z.string().describe("Its lifecycle state — `active`, `refunded`, `expired`, and so on."),
|
|
110
|
+
environment: z.string().describe("Whether it happened in the store's sandbox rather than production."),
|
|
111
|
+
purchasedAt: z.iso.datetime().describe("When it was bought, ISO-8601."),
|
|
112
|
+
expiresAt: z.iso.datetime().nullable().describe("When it runs out, ISO-8601; null for something owned forever."),
|
|
113
|
+
revokedAt: z.iso.datetime().nullable().describe("When it was refunded or revoked, ISO-8601; null while it stands.")
|
|
114
|
+
}).describe("One purchase beside the sender, so an operator answering a billing question need not go and look.");
|
|
115
|
+
/** One of the sender's entitlements, lapsed ones included and marked inactive. */
|
|
116
|
+
const SenderEntitlementView = z.object({
|
|
117
|
+
key: z.string().describe("The entitlement key."),
|
|
118
|
+
active: z.boolean().describe("Whether it granted when the projection last wrote it."),
|
|
119
|
+
expiresAt: z.iso.datetime().nullable().describe("When the grant lapses, ISO-8601; null when it never does."),
|
|
120
|
+
source: z.string().nullable().describe("Opaque provenance — a purchase id, or a support grant.")
|
|
121
|
+
}).describe("One entitlement beside the sender. Lapsed ones are here too — a paywall wants to say when Pro ended.");
|
|
122
|
+
/**
|
|
123
|
+
* Which billing subject the purchase and entitlement lists cover — always `user`, and said out loud.
|
|
124
|
+
*
|
|
125
|
+
* `@pithy-sh/payments` keys a purchase on a **subject pair**, a user or an organization. Support starts
|
|
126
|
+
* from a `From:` header and resolves a *person*; at thread-read time it holds no Hono `Context`, so it
|
|
127
|
+
* cannot ask the adopter's subject seam which organization this caller is acting for. It therefore reads
|
|
128
|
+
* `user`-subject rows and nothing else.
|
|
129
|
+
*
|
|
130
|
+
* **A field rather than a footnote, because empty already means too many things.** The purchase and
|
|
131
|
+
* entitlement lookups are guarded dynamic imports whose `catch` returns `[]`, so an empty panel is
|
|
132
|
+
* indistinguishable between "bought nothing", "`@pithy-sh/payments` is not installed" and "billed to an
|
|
133
|
+
* organization this seam cannot see" — and an operator looking at a customer with a live subscription
|
|
134
|
+
* reads it as the first. That is the actual hazard, and it is a wrong refund decision away from costing
|
|
135
|
+
* something. Declaring the scope on the wire lets a console say *why* the panel is thin.
|
|
136
|
+
*
|
|
137
|
+
* **A closed literal, not a string.** Widening it is then a compile error at every consumer, which is
|
|
138
|
+
* correct: the day support can resolve an organization, every panel has to decide what to render.
|
|
139
|
+
*/
|
|
140
|
+
const SenderBillingScope = z.literal(SUPPORT_BILLING_SCOPE).describe("Which billing subject the purchase and entitlement lists cover. Always `user`: support resolves a person from an address and cannot reach the adopter's subject seam, so an organization-billed customer's purchases are not shown here.");
|
|
141
|
+
/**
|
|
142
|
+
* What the app already knows about the sender. Derived at read time, so it is always current.
|
|
143
|
+
*
|
|
144
|
+
* **Everything but `authenticated` and `billingScope` is empty when the sender is unproven.** The whole
|
|
145
|
+
* value of the panel is that an operator trusts it, so it must not be populated from an address anybody
|
|
146
|
+
* could have written — that is what turns a spoofed message into support-driven account takeover. The
|
|
147
|
+
* scope is declared either way: it describes what this seam *can* see, not what it found.
|
|
148
|
+
*/
|
|
149
|
+
const SenderContextView = z.object({
|
|
150
|
+
authenticated: z.boolean().describe("Whether the `From:` header was proved to belong to the sender."),
|
|
151
|
+
userId: z.string().nullable().describe("The linked user id, or null when this address belongs to nobody."),
|
|
152
|
+
name: z.string().optional().describe("The account's display name, when auth is composed and the sender is known."),
|
|
153
|
+
emailVerified: z.boolean().optional().describe("Whether the account has verified this address."),
|
|
154
|
+
billingScope: SenderBillingScope,
|
|
155
|
+
purchases: z.array(SenderPurchaseView).describe("Their purchase history, newest first, bounded. Empty when none."),
|
|
156
|
+
entitlements: z.array(SenderEntitlementView).describe("Their entitlements, lapsed ones marked inactive.")
|
|
157
|
+
}).describe("The customer context beside a conversation, withheld entirely when the sender is unproven.");
|
|
158
|
+
/** One canned reply, ordered for this thread's category. */
|
|
159
|
+
const SupportReplyView = z.object({
|
|
160
|
+
key: z.string().describe("The snippet's key in the catalog."),
|
|
161
|
+
label: z.string().describe("What the picker shows, e.g. `Refund issued`."),
|
|
162
|
+
category: z.string().optional().describe("The category it is offered first for. Absent means always offered."),
|
|
163
|
+
body: z.string().describe("The reply text, ready to edit.")
|
|
164
|
+
}).describe("One canned reply from the effective catalog.");
|
|
165
|
+
/** `GET {base}/threads`. */
|
|
166
|
+
const SupportThreadsResponse = z.object({
|
|
167
|
+
threads: z.array(SupportListedThreadView).describe("The page, newest first."),
|
|
168
|
+
nextCursor: NextCursor
|
|
169
|
+
}).describe("A page of the inbox, as one viewer sees it.");
|
|
170
|
+
/** `GET {base}/threads/:id`. */
|
|
171
|
+
const SupportThreadResponse = z.object({
|
|
172
|
+
thread: SupportThreadView.describe("The conversation."),
|
|
173
|
+
messages: z.array(SupportMessageView).describe("Its messages, oldest first — a conversation reads downward."),
|
|
174
|
+
attachments: z.array(SupportAttachmentView).describe("Its attachments, across every message."),
|
|
175
|
+
sender: SenderContextView.describe("What the app already knows about the sender."),
|
|
176
|
+
replies: z.array(SupportReplyView).describe("The canned replies, ordered for this thread's category.")
|
|
177
|
+
}).describe("A whole conversation, with the customer context and the replies worth offering on it.");
|
|
178
|
+
/**
|
|
179
|
+
* `POST {base}/threads/:id/archive`.
|
|
180
|
+
*
|
|
181
|
+
* The thread itself rather than an envelope around it: archiving is a write whose entire result is the
|
|
182
|
+
* new state of the row, and a pane re-renders from it.
|
|
183
|
+
*/
|
|
184
|
+
const SupportArchiveResponse = SupportThreadView.describe("The conversation, as the archive left it.");
|
|
185
|
+
/**
|
|
186
|
+
* `POST {base}/threads/:id/reply`.
|
|
187
|
+
*
|
|
188
|
+
* **A union on `channel`, not an object with an optional `jobId`.** A reply either left by mail or was
|
|
189
|
+
* stored for the submitter to read in the app, and those are different promises about when somebody
|
|
190
|
+
* sees the answer. An optional field is exactly what lets a console render "sent" over both of them,
|
|
191
|
+
* so the two arms are structurally different and a client has to branch to read either one.
|
|
192
|
+
*/
|
|
193
|
+
const SupportReplySentResponse = z.discriminatedUnion("channel", [z.object({
|
|
194
|
+
channel: z.literal("email").describe("The reply was handed to the durable send path. It is on its way."),
|
|
195
|
+
messageId: z.string().describe("The outbound message row this reply created."),
|
|
196
|
+
jobId: z.string().describe("The email job it was enqueued as. The send itself is a Workflow's job.")
|
|
197
|
+
}).describe("A reply that was queued for sending, by message and by mail job."), z.object({
|
|
198
|
+
channel: z.literal("app").describe("The reply was stored, and no mail was sent. The submitter reads it next time they open the conversation — which means nobody has told them yet, and telling them is the application's call."),
|
|
199
|
+
messageId: z.string().describe("The outbound message row this reply created.")
|
|
200
|
+
}).describe("A reply that is waiting in the app. There is no mail job, because there was no send.")]).describe("The reply that was filed, and which of the two ways it reaches the customer.");
|
|
201
|
+
/** `POST {base}/threads/:id/reclassify`. */
|
|
202
|
+
const SupportReclassifiedResponse = z.object({ messageId: z.string().describe("The latest inbound message the classifier was started against.") }).describe("Which message a reclassification was started for.");
|
|
203
|
+
/**
|
|
204
|
+
* `POST {base}/threads/:id/flags`.
|
|
205
|
+
*
|
|
206
|
+
* `ok` and nothing else. A private read flag is not audited and carries no state a client did not
|
|
207
|
+
* just send, so echoing the thread back would only invite somebody to render it.
|
|
208
|
+
*/
|
|
209
|
+
const SupportFlagsResponse = z.object({ ok: z.literal(true).describe("Always true. The write either happened or the request failed.") }).describe("The acknowledgement of one viewer's private flags.");
|
|
210
|
+
/**
|
|
211
|
+
* ## The reader's contracts, beside the producers above
|
|
212
|
+
*
|
|
213
|
+
* Everything above this line is stated for a Worker checking its own projection, and it is strict on
|
|
214
|
+
* purpose: `SupportChannel` and `SupportAccountLinkSource` are what `inbound/` branches on and what D1
|
|
215
|
+
* holds, and a tolerated-unknown member there would license this capability to *store* a channel it
|
|
216
|
+
* cannot handle. Nothing below changes any of that. The enums are untouched, and so is every schema
|
|
217
|
+
* above.
|
|
218
|
+
*
|
|
219
|
+
* Below is the same projection for the other consumer: a management client reading a Worker **it does
|
|
220
|
+
* not control**. That client is crossing a trust boundary — a fork, a bug, a half-finished deploy, or a
|
|
221
|
+
* hostile Worker each put a member here that no enum of ours declares — and under the strict schema one
|
|
222
|
+
* such token costs it the entire response. It cost `pithy-sh/dashboard#15` exactly that: one
|
|
223
|
+
* unrecognized `channel` and the support pane rendered zero of twenty-five conversations. The client
|
|
224
|
+
* then had to widen our shape locally, which is the hand-held mirror `#113` exists to forbid.
|
|
225
|
+
*
|
|
226
|
+
* So the tolerance is published rather than reimplemented, by the one pattern
|
|
227
|
+
* `@pithy-sh/core/src/projection/asRead` states for the whole kit: same objects, every enum read as a
|
|
228
|
+
* string, every other field the identical schema instance.
|
|
229
|
+
*
|
|
230
|
+
* **What a client does with a widened value is ask the enum.** `SupportChannel.safeParse(value).success`
|
|
231
|
+
* answers *does this capability declare it*, and a value it does not declare is **marked, never mapped**
|
|
232
|
+
* — rendering an unknown provenance as the nearest one you know is a lie about evidence on the screen
|
|
233
|
+
* where somebody decides whether to act on a stranger's request.
|
|
234
|
+
*
|
|
235
|
+
* **Only the management surface.** The submitter's own views stay strict: an app reading its own
|
|
236
|
+
* Worker's `/feedback` is not reading a stranger, and `SupportReplySentResponse` is a union discriminated
|
|
237
|
+
* on a literal the client itself sent, where tolerating an unknown arm would mean tolerating a response
|
|
238
|
+
* with no shape at all.
|
|
239
|
+
*/
|
|
240
|
+
/** {@link SupportThreadView}, as a client that does not control the Worker must read it. */
|
|
241
|
+
const SupportThreadViewAsRead = asRead(SupportThreadView);
|
|
242
|
+
/** {@link SupportListedThreadView}, as a client that does not control the Worker must read it. */
|
|
243
|
+
const SupportListedThreadViewAsRead = asRead(SupportListedThreadView);
|
|
244
|
+
/** {@link SupportMessageView}, as a client that does not control the Worker must read it. */
|
|
245
|
+
const SupportMessageViewAsRead = asRead(SupportMessageView);
|
|
246
|
+
/** `GET {base}/threads`, as a client that does not control the Worker must read it. */
|
|
247
|
+
const SupportThreadsResponseAsRead = asRead(SupportThreadsResponse);
|
|
248
|
+
/** `GET {base}/threads/:id`, as a client that does not control the Worker must read it. */
|
|
249
|
+
const SupportThreadResponseAsRead = asRead(SupportThreadResponse);
|
|
250
|
+
/** `POST {base}/threads/:id/archive`, as a client that does not control the Worker must read it. */
|
|
251
|
+
const SupportArchiveResponseAsRead = asRead(SupportArchiveResponse);
|
|
252
|
+
/**
|
|
253
|
+
* ## The submitter's own view, and what it deliberately omits
|
|
254
|
+
*
|
|
255
|
+
* Everything above this line is written for a **management client**: an operator triaging an inbox,
|
|
256
|
+
* who needs the classification to sort by and the sender's purchase history to act on. Everything
|
|
257
|
+
* below is written for the **person who wrote the message**, and it is a different object rather than
|
|
258
|
+
* the same one with fields nulled — because a projection that starts from the operator's shape leaks
|
|
259
|
+
* the first time somebody adds a column and forgets which of the two callers is reading.
|
|
260
|
+
*
|
|
261
|
+
* A submitter is shown their own words, the answers to them, and whether the thread is done. They are
|
|
262
|
+
* never shown the classification (a machine's judgment about them, and a filter on it is the inbox's
|
|
263
|
+
* business), the priority or sentiment (the same, and `angry` rendered back to the person it describes
|
|
264
|
+
* is its own kind of disaster), the per-viewer flags (private to an operator), the account link or its
|
|
265
|
+
* provenance (they know who they are), or anything at all about another account.
|
|
266
|
+
*/
|
|
267
|
+
/** One of the submitter's own conversations. */
|
|
268
|
+
const SupportMyThreadView = z.object({
|
|
269
|
+
id: z.string().describe("The conversation's id — what the read-back and a follow-up submission take."),
|
|
270
|
+
subject: z.string().describe("What they called it when they opened it. Later messages never rewrite it."),
|
|
271
|
+
resolved: z.boolean().describe("Whether support has marked this done. Named for the reader rather than mirroring the column: `archived` is an inbox's word for a thread it has finished with, and to the person waiting on an answer the fact is that it was resolved. Writing again reopens it."),
|
|
272
|
+
messageCount: z.number().int().describe("How many messages the conversation holds, theirs and the answers."),
|
|
273
|
+
lastMessageAt: z.iso.datetime().describe("When the conversation last moved, ISO-8601. The list sorts on this."),
|
|
274
|
+
createdAt: z.iso.datetime().describe("When they opened it, ISO-8601.")
|
|
275
|
+
}).describe("One of the caller's own in-app support conversations, as the person who opened it sees it.");
|
|
276
|
+
/** One message in the submitter's own conversation. */
|
|
277
|
+
const SupportMyMessageView = z.object({
|
|
278
|
+
id: z.string().describe("The message's id."),
|
|
279
|
+
direction: SupportMessageDirection.describe("`inbound` is theirs, `outbound` is the answer. The only identity on this view: who answered is a person's name in the body if they signed it, and never an operator's account."),
|
|
280
|
+
body: z.string().describe("The message text. Plain text on both sides — what they sent, and what was sent back. Never HTML, so a client renders it escaped and there is nothing here to sanitize."),
|
|
281
|
+
context: SupportSubmissionContext.nullable().describe("The context their app attached to this message, so a client can show what was sent on their behalf. Null on an answer."),
|
|
282
|
+
attachments: z.array(SupportAttachmentView).describe("What they attached to this message. Empty on an answer."),
|
|
283
|
+
sentAt: z.iso.datetime().describe("When it was sent, ISO-8601.")
|
|
284
|
+
}).describe("One message in the caller's own conversation — their words, or the answer to them.");
|
|
285
|
+
/** `POST {base}/feedback`. */
|
|
286
|
+
const SupportSubmissionResponse = z.object({
|
|
287
|
+
threadId: z.string().describe("The conversation it landed in — a new one, or the one it continued."),
|
|
288
|
+
messageId: z.string().describe("The message that was stored."),
|
|
289
|
+
opened: z.boolean().describe("True when this opened a conversation, false when it continued one. A client that sent no `threadId` and gets false has hit nothing surprising — it cannot happen — so this is here for the client that sent one and wants to confirm which."),
|
|
290
|
+
attachments: z.number().int().describe("How many attachments were stored. Reported rather than assumed: a deployment with no bucket bound stores the report and none of its files, and a client that showed a paperclip should be able to say so.")
|
|
291
|
+
}).describe("What an in-app support request produced.");
|
|
292
|
+
/** `GET {base}/feedback`. */
|
|
293
|
+
const SupportMyThreadsResponse = z.object({
|
|
294
|
+
threads: z.array(SupportMyThreadView).describe("The caller's own conversations, newest first."),
|
|
295
|
+
nextCursor: NextCursor
|
|
296
|
+
}).describe("A page of the caller's own support conversations.");
|
|
297
|
+
/** `GET {base}/feedback/:id`. */
|
|
298
|
+
const SupportMyThreadResponse = z.object({
|
|
299
|
+
thread: SupportMyThreadView.describe("The conversation."),
|
|
300
|
+
messages: z.array(SupportMyMessageView).describe("Its messages, oldest first — a conversation reads downward.")
|
|
301
|
+
}).describe("One of the caller's own conversations, in full.");
|
|
302
|
+
/** `GET {base}/replies`. */
|
|
303
|
+
const SupportRepliesResponse = z.object({ replies: z.array(SupportReplyView).describe("The catalog, ordered for the requested category.") }).describe("The canned reply catalog.");
|
|
304
|
+
//#endregion
|
|
305
|
+
export { SenderBillingScope, SenderContextView, SenderEntitlementView, SenderPurchaseView, SupportArchiveResponse, SupportArchiveResponseAsRead, SupportAttachmentView, SupportFlagsResponse, SupportListedThreadView, SupportListedThreadViewAsRead, SupportMessageView, SupportMessageViewAsRead, SupportMyMessageView, SupportMyThreadResponse, SupportMyThreadView, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportRepliesResponse, SupportReplySentResponse, SupportReplyView, SupportSubmissionResponse, SupportThreadResponse, SupportThreadResponseAsRead, SupportThreadView, SupportThreadViewAsRead, SupportThreadsResponse, SupportThreadsResponseAsRead };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
4
|
+
import type { Context, Hono } from "hono";
|
|
5
|
+
import { type HandlerDeps } from "./handlers";
|
|
6
|
+
/**
|
|
7
|
+
* The support routes, their verification strategies, and what each accepts:
|
|
8
|
+
*
|
|
9
|
+
* GET /support/threads → the inbox (control-plane: support:threads:read) query: ListThreadsQuery
|
|
10
|
+
* GET /support/threads/:id → one thread (control-plane: support:threads:read) param: ThreadIdParam
|
|
11
|
+
* POST /support/threads/:id/archive → done/reopen (control-plane: support:threads:archive) param + json
|
|
12
|
+
* POST /support/threads/:id/reply → answer (control-plane: support:threads:reply) param + json
|
|
13
|
+
* POST /support/threads/:id/reclassify→ re-run model (control-plane: support:threads:reclassify) param
|
|
14
|
+
* POST /support/threads/:id/flags → read/snooze (control-plane: support:threads:flag) param + json
|
|
15
|
+
* GET /support/replies → canned copy (control-plane: support:threads:read) query: RepliesQuery
|
|
16
|
+
*
|
|
17
|
+
* POST /support/feedback → write in (bearer/session + same-origin) json: SubmitFeedbackInput
|
|
18
|
+
* GET /support/feedback → my requests (bearer/session) query: MyThreadsQuery
|
|
19
|
+
* GET /support/feedback/:id → one of mine (bearer/session) param: ThreadIdParam
|
|
20
|
+
*
|
|
21
|
+
* **Two surfaces, two gates, and they never stack.** The management routes answer to a control-plane
|
|
22
|
+
* credential the adopter issued, and with the seam uncomposed every one denies with
|
|
23
|
+
* `controlplane/not_connected` — the correct failure for an inbox holding other people's private
|
|
24
|
+
* 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. A control-plane caller holds no
|
|
26
|
+
* session by design and can never satisfy `requireAuth()`; a user's session confers no scope. See
|
|
27
|
+
* `guards.ts`.
|
|
28
|
+
*
|
|
29
|
+
* **`requireSameOrigin()` is on the submission route and on neither read.** Cookie-mode sessions make a
|
|
30
|
+
* mutating route CSRF-reachable, and this one writes into a support inbox under a real customer's name
|
|
31
|
+
* — a forged submission is somebody else's words attributed to them, in the one place an operator
|
|
32
|
+
* treats attribution as proven. The reads are GETs and carry no such risk. A bearer caller is
|
|
33
|
+
* CSRF-exempt, and that exemption belongs to the gate `@pithy-sh/auth` publishes rather than to
|
|
34
|
+
* anything decided here.
|
|
35
|
+
*
|
|
36
|
+
* **Every response has an exported schema**, in `responses.ts`. Each handler's return type is
|
|
37
|
+
* `z.output` of its envelope, so `c.json(await handler(...))` carries the contract without a cast — and
|
|
38
|
+
* a management client imports the same object and validates with it rather than hand-writing a mirror
|
|
39
|
+
* that drifts.
|
|
40
|
+
*
|
|
41
|
+
* The validators sit **after** the gate on every line. A validator ahead of it turns a 401 into a
|
|
42
|
+
* 400 and tells an unverified caller which requests were well-formed — and on this surface that is a
|
|
43
|
+
* live oracle for the shape of an adopter's support tooling.
|
|
44
|
+
*
|
|
45
|
+
* ## An adopter's own authorization on the submission route
|
|
46
|
+
*
|
|
47
|
+
* **This capability gates `POST {base}/feedback` on a session and same-origin, and on nothing else,
|
|
48
|
+
* permanently.** Writing to support must not be role-gated or it stops being a general intake: the
|
|
49
|
+
* person who most needs to reach support is often the one whose access is broken, and a role the kit
|
|
50
|
+
* invented would make one adopter's account model a condition on everybody's ability to report a bug.
|
|
51
|
+
*
|
|
52
|
+
* But an adopter whose *own* model makes some submissions act-on-behalf-of — `pithy-sh/dashboard#10`'s
|
|
53
|
+
* discount application is made for an organization, and a member may not make one — needs somewhere to
|
|
54
|
+
* put that check, and it must not be the client. **The seam for it already exists, in the composition
|
|
55
|
+
* contract rather than in this capability's config**, and it is documented here because it was not
|
|
56
|
+
* discoverable rather than because it was missing:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* // the adopter's own `app` capability
|
|
60
|
+
* defineCapability({
|
|
61
|
+
* name: "app",
|
|
62
|
+
* middleware: [
|
|
63
|
+
* (app) => {
|
|
64
|
+
* app.use("/support/feedback", async (c, next) => {
|
|
65
|
+
* // c.var.auth is already populated — @pithy-sh/auth's session middleware is a library's, and
|
|
66
|
+
* // every capability's middleware mounts before any capability's routes.
|
|
67
|
+
* if (c.var.auth && !(await mayWriteOnBehalfOfTheOrganization(c))) throw new ForbiddenError({ … });
|
|
68
|
+
* await next();
|
|
69
|
+
* });
|
|
70
|
+
* },
|
|
71
|
+
* ],
|
|
72
|
+
* …
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* `createBackend` mounts **every** capability's middleware before **any** capability's routes, and the
|
|
77
|
+
* adopter's `app` capability composes last — so their middleware runs after auth has resolved the
|
|
78
|
+
* session and before this file's `requireAuth()`. `createBackend.workers.test.ts` pins that ordering,
|
|
79
|
+
* because a paragraph asserting it is not the same as a test failing when it changes.
|
|
80
|
+
*
|
|
81
|
+
* Two consequences worth stating rather than discovering. Their middleware sees `c.var.auth` as **null**
|
|
82
|
+
* on an unauthenticated request, since it runs ahead of the route's own gate — so it should pass those
|
|
83
|
+
* through and let `requireAuth()` answer 401, rather than 403 a caller who was never signed in. And the
|
|
84
|
+
* path is theirs to write, from the `basePath` they configured; a mount point they changed and a
|
|
85
|
+
* middleware path they did not is a gate that silently stops covering anything.
|
|
86
|
+
*
|
|
87
|
+
* **A `beforeSubmit` callback in `SupportConfig` was the tempting alternative and is the wrong shape.**
|
|
88
|
+
* The need is not support's — every capability with a write route has it — so solving it once per
|
|
89
|
+
* capability would give an adopter a different mechanism per package, each with its own signature and
|
|
90
|
+
* its own answer to "what is in scope here". A config file is also the wrong home for an authorization
|
|
91
|
+
* decision that wants `c.var`, and a second place to look for the gates on a route is how a Worker ends
|
|
92
|
+
* up with two of them, free to disagree.
|
|
93
|
+
*/
|
|
94
|
+
/** How the support sub-router is built. */
|
|
95
|
+
export interface SupportRoutesOptions {
|
|
96
|
+
/** The path the routes mount under. Defaults to `/support`. */
|
|
97
|
+
basePath?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Whether to mount the in-app submission routes. Defaults to true, matching `submission.enabled`.
|
|
100
|
+
*
|
|
101
|
+
* Not mounting is deliberate rather than a guard inside the handlers: a route that is not served
|
|
102
|
+
* answers 404, and 404 is the honest answer. A 403 would say "this exists and you may not use it" to
|
|
103
|
+
* a caller asking about a feature this deployment does not have.
|
|
104
|
+
*/
|
|
105
|
+
submission?: boolean;
|
|
106
|
+
/** Resolve handler deps from the request context. */
|
|
107
|
+
resolveDeps: (c: Context<PithyHonoEnv>) => Promise<HandlerDeps>;
|
|
108
|
+
}
|
|
109
|
+
/** Register the support sub-router. Returned as the capability's `routes` hook. */
|
|
110
|
+
export declare function registerSupportRoutes(options: SupportRoutesOptions): (app: Hono<PithyHonoEnv>) => void;
|
|
111
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAK7E,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,EAEL,KAAK,WAAW,EAUjB,MAAM,YAAY,CAAC;AAoBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CACjE;AAuCD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,CAqFtG"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
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 { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput, ThreadIdParam } from "./schemas.js";
|
|
6
|
+
import { SUPPORT_THREADS_ARCHIVE_SCOPE, SUPPORT_THREADS_FLAG_SCOPE, SUPPORT_THREADS_READ_SCOPE, SUPPORT_THREADS_RECLASSIFY_SCOPE, SUPPORT_THREADS_REPLY_SCOPE } from "./scopes.js";
|
|
7
|
+
import { InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
8
|
+
import { zValidator } from "@hono/zod-validator";
|
|
9
|
+
import { requireControlPlane } from "@pithy-sh/core/src/controlPlane/http/guard";
|
|
10
|
+
import { requireSameOrigin } from "@pithy-sh/core/src/http/sameOrigin";
|
|
11
|
+
import { validationHook } from "@pithy-sh/core/src/http/validation";
|
|
12
|
+
//#region src/http/routes.ts
|
|
13
|
+
/**
|
|
14
|
+
* The verified management client behind a control-plane call.
|
|
15
|
+
*
|
|
16
|
+
* `requireControlPlane()` has run on every route, so `c.var.controlPlane` is populated by the time a
|
|
17
|
+
* handler reads it. The throw is a programming-error guard, not a runtime path: reaching it would
|
|
18
|
+
* mean a route was mounted without its gate, which is the one mistake this whole file is arranged to
|
|
19
|
+
* make impossible.
|
|
20
|
+
*/
|
|
21
|
+
function viewer(c) {
|
|
22
|
+
const subject = c.var.controlPlane?.subject;
|
|
23
|
+
if (!subject) throw new InternalError({
|
|
24
|
+
message: "Support could not identify the management caller.",
|
|
25
|
+
detail: "requireControlPlane() must run before a support handler reads the caller."
|
|
26
|
+
});
|
|
27
|
+
return subject;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The signed-in user behind an in-app submission.
|
|
31
|
+
*
|
|
32
|
+
* `requireAuth()` has run on every route that calls this, so `c.var.auth` is populated by the time a
|
|
33
|
+
* handler reads it. The throw is a programming-error guard rather than a runtime path — the mirror of
|
|
34
|
+
* {@link viewer}, and the reason neither handler ever has to think about an absent caller.
|
|
35
|
+
*/
|
|
36
|
+
function submitter(c) {
|
|
37
|
+
const auth = c.var.auth;
|
|
38
|
+
if (!auth) throw new InternalError({
|
|
39
|
+
message: "Support could not identify the signed-in caller.",
|
|
40
|
+
detail: "requireAuth() must run before a support submission handler reads the submitter."
|
|
41
|
+
});
|
|
42
|
+
return auth.userId;
|
|
43
|
+
}
|
|
44
|
+
/** Register the support sub-router. Returned as the capability's `routes` hook. */
|
|
45
|
+
function registerSupportRoutes(options) {
|
|
46
|
+
const base = options.basePath ?? "/support";
|
|
47
|
+
const resolve = options.resolveDeps;
|
|
48
|
+
const submission = options.submission ?? true;
|
|
49
|
+
return (app) => {
|
|
50
|
+
app.get(`${base}/threads`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("query", ListThreadsQuery, validationHook), async (c) => c.json(await listInbox(await resolve(c), c.req.valid("query"), viewer(c))));
|
|
51
|
+
app.get(`${base}/replies`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("query", RepliesQuery, validationHook), async (c) => c.json({ replies: listReplies(await resolve(c), c.req.valid("query")) }));
|
|
52
|
+
app.get(`${base}/threads/:id`, requireControlPlane(SUPPORT_THREADS_READ_SCOPE), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await readConversation(await resolve(c), c.req.valid("param").id)));
|
|
53
|
+
app.post(`${base}/threads/:id/archive`, requireControlPlane(SUPPORT_THREADS_ARCHIVE_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", ArchiveThreadInput, validationHook), async (c) => c.json(await archiveConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
|
|
54
|
+
app.post(`${base}/threads/:id/reply`, requireControlPlane(SUPPORT_THREADS_REPLY_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", ReplyInput, validationHook), async (c) => c.json(await replyToConversation(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c)), 201));
|
|
55
|
+
app.post(`${base}/threads/:id/reclassify`, requireControlPlane(SUPPORT_THREADS_RECLASSIFY_SCOPE), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await reclassifyConversation(await resolve(c), c.req.valid("param").id, viewer(c))));
|
|
56
|
+
app.post(`${base}/threads/:id/flags`, requireControlPlane(SUPPORT_THREADS_FLAG_SCOPE), zValidator("param", ThreadIdParam, validationHook), zValidator("json", FlagsInput, validationHook), async (c) => c.json(await updateFlags(await resolve(c), c.req.valid("param").id, c.req.valid("json"), viewer(c))));
|
|
57
|
+
if (!submission) return;
|
|
58
|
+
app.post(`${base}/feedback`, requireAuth(), requireSameOrigin(), zValidator("json", SubmitFeedbackInput, validationHook), async (c) => c.json(await submitFeedbackRequest(await resolve(c), c.req.valid("json"), submitter(c)), 201));
|
|
59
|
+
app.get(`${base}/feedback`, requireAuth(), zValidator("query", MyThreadsQuery, validationHook), async (c) => c.json(await listMyThreads(await resolve(c), c.req.valid("query"), submitter(c))));
|
|
60
|
+
app.get(`${base}/feedback/:id`, requireAuth(), zValidator("param", ThreadIdParam, validationHook), async (c) => c.json(await readMyThread(await resolve(c), c.req.valid("param").id, submitter(c))));
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { registerSupportRoutes };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* The request contracts every support route declares (CLAUDE.md §HTTP).
|
|
6
|
+
*
|
|
7
|
+
* Every one of these is a **bound on something a caller chose**, which is the whole job: a control-
|
|
8
|
+
* plane credential is verified, but verified is not the same as trusted, and a management client with
|
|
9
|
+
* a bug can ask for a million rows as easily as a hostile one can.
|
|
10
|
+
*
|
|
11
|
+
* The category filter is a bounded string rather than an enum, and that is deliberate: the taxonomy
|
|
12
|
+
* is federated, so the valid set is not known here — and building the schema from the configured set
|
|
13
|
+
* would make a filter for a category an adopter just removed a 400 instead of an empty list, which is
|
|
14
|
+
* a worse answer to the same question. The value is only ever compared, never interpreted.
|
|
15
|
+
*/
|
|
16
|
+
/** A thread id in the path. */
|
|
17
|
+
export declare const ThreadIdParam: z.ZodObject<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export type ThreadIdParam = z.infer<typeof ThreadIdParam>;
|
|
21
|
+
/** The inbox listing query. */
|
|
22
|
+
export declare const ListThreadsQuery: z.ZodObject<{
|
|
23
|
+
archived: z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
false: "false";
|
|
25
|
+
true: "true";
|
|
26
|
+
}>>;
|
|
27
|
+
category: z.ZodOptional<z.ZodString>;
|
|
28
|
+
declaredCategory: z.ZodOptional<z.ZodString>;
|
|
29
|
+
priority: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
low: "low";
|
|
31
|
+
normal: "normal";
|
|
32
|
+
urgent: "urgent";
|
|
33
|
+
}>>;
|
|
34
|
+
sentiment: z.ZodOptional<z.ZodEnum<{
|
|
35
|
+
angry: "angry";
|
|
36
|
+
frustrated: "frustrated";
|
|
37
|
+
neutral: "neutral";
|
|
38
|
+
positive: "positive";
|
|
39
|
+
}>>;
|
|
40
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
41
|
+
app: "app";
|
|
42
|
+
email: "email";
|
|
43
|
+
}>>;
|
|
44
|
+
inbox: z.ZodOptional<z.ZodString>;
|
|
45
|
+
q: z.ZodOptional<z.ZodString>;
|
|
46
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
47
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type ListThreadsQuery = z.infer<typeof ListThreadsQuery>;
|
|
50
|
+
/** The archive/unarchive body. */
|
|
51
|
+
export declare const ArchiveThreadInput: z.ZodObject<{
|
|
52
|
+
archived: z.ZodBoolean;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
export type ArchiveThreadInput = z.infer<typeof ArchiveThreadInput>;
|
|
55
|
+
/** The reply body. */
|
|
56
|
+
export declare const ReplyInput: z.ZodObject<{
|
|
57
|
+
body: z.ZodString;
|
|
58
|
+
agentName: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export type ReplyInput = z.infer<typeof ReplyInput>;
|
|
61
|
+
/** The per-viewer flags body. */
|
|
62
|
+
export declare const FlagsInput: z.ZodObject<{
|
|
63
|
+
read: z.ZodOptional<z.ZodBoolean>;
|
|
64
|
+
snoozedUntil: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export type FlagsInput = z.infer<typeof FlagsInput>;
|
|
67
|
+
/**
|
|
68
|
+
* One file on an in-app submission.
|
|
69
|
+
*
|
|
70
|
+
* The bytes arrive base64-encoded in JSON so the whole request stays one Zod object on the route line,
|
|
71
|
+
* like every other route in this capability. **The size and type bounds are not here**: they are the
|
|
72
|
+
* adopter's `submission.attachments` config, resolved in the handler, because a request schema is built
|
|
73
|
+
* once at module load and cannot know them. What this bounds is the shape — a filename that renders, a
|
|
74
|
+
* MIME type that looks like one, and a payload that is base64 at all.
|
|
75
|
+
*/
|
|
76
|
+
export declare const SubmittedAttachmentInput: z.ZodObject<{
|
|
77
|
+
filename: z.ZodString;
|
|
78
|
+
contentType: z.ZodString;
|
|
79
|
+
data: z.ZodUnion<readonly [z.ZodBase64, z.ZodBase64URL]>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
export type SubmittedAttachmentInput = z.infer<typeof SubmittedAttachmentInput>;
|
|
82
|
+
/**
|
|
83
|
+
* The in-app submission body.
|
|
84
|
+
*
|
|
85
|
+
* **Nothing here names an account, and nothing here could.** The submitter is `c.var.auth.userId`,
|
|
86
|
+
* proved before this schema ran — a `userId` field would be a client-supplied identity on the one
|
|
87
|
+
* surface whose whole argument is that it does not need one.
|
|
88
|
+
*
|
|
89
|
+
* The lengths are the hard ceilings from `config/config.ts`, not the adopter's configured bounds: this
|
|
90
|
+
* schema is built once at module load, and the configured numbers are applied in the handler where the
|
|
91
|
+
* resolved config lives. A body over the ceiling never reaches a handler at all.
|
|
92
|
+
*/
|
|
93
|
+
export declare const SubmitFeedbackInput: z.ZodObject<{
|
|
94
|
+
subject: z.ZodString;
|
|
95
|
+
body: z.ZodString;
|
|
96
|
+
declaredCategory: z.ZodOptional<z.ZodString>;
|
|
97
|
+
threadId: z.ZodOptional<z.ZodString>;
|
|
98
|
+
context: z.ZodOptional<z.ZodObject<{
|
|
99
|
+
screen: z.ZodOptional<z.ZodString>;
|
|
100
|
+
appVersion: z.ZodOptional<z.ZodString>;
|
|
101
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
102
|
+
environment: z.ZodOptional<z.ZodString>;
|
|
103
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, z.core.$strict>>;
|
|
105
|
+
attachments: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
106
|
+
filename: z.ZodString;
|
|
107
|
+
contentType: z.ZodString;
|
|
108
|
+
data: z.ZodUnion<readonly [z.ZodBase64, z.ZodBase64URL]>;
|
|
109
|
+
}, z.core.$strip>>>;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
export type SubmitFeedbackInput = z.infer<typeof SubmitFeedbackInput>;
|
|
112
|
+
/** The submitter's own thread list. */
|
|
113
|
+
export declare const MyThreadsQuery: z.ZodObject<{
|
|
114
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
115
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
116
|
+
}, z.core.$strip>;
|
|
117
|
+
export type MyThreadsQuery = z.infer<typeof MyThreadsQuery>;
|
|
118
|
+
/** The canned-reply catalog query. */
|
|
119
|
+
export declare const RepliesQuery: z.ZodObject<{
|
|
120
|
+
category: z.ZodOptional<z.ZodString>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
export type RepliesQuery = z.infer<typeof RepliesQuery>;
|
|
123
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/http/schemas.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;;;;;;;;;;GAWG;AAEH,+BAA+B;AAC/B,eAAO,MAAM,aAAa;;iBAOsC,CAAC;AACjE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAY1D,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwC8D,CAAC;AAC5F,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,kCAAkC;AAClC,eAAO,MAAM,kBAAkB;;iBAQuB,CAAC;AACvD,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,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"}
|