@pithy-sh/support 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/classify.d.ts +90 -0
- package/dist/ai/classify.d.ts.map +1 -0
- package/dist/ai/classify.js +151 -0
- package/dist/attachment/store.d.ts +53 -0
- package/dist/attachment/store.d.ts.map +1 -0
- package/dist/attachment/store.js +67 -0
- package/dist/audit/actions.d.ts +68 -0
- package/dist/audit/actions.d.ts.map +1 -0
- package/dist/audit/actions.js +68 -0
- package/dist/capability.d.ts +93 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +187 -0
- package/dist/client/projection.d.ts +56 -0
- package/dist/client/projection.d.ts.map +1 -0
- package/dist/client/projection.js +0 -0
- package/dist/config/config.d.ts +215 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +169 -0
- package/dist/data/attachment.d.ts +30 -0
- package/dist/data/attachment.d.ts.map +1 -0
- package/dist/data/attachment.js +32 -0
- package/dist/data/billingScope.d.ts +30 -0
- package/dist/data/billingScope.d.ts.map +1 -0
- package/dist/data/billingScope.js +32 -0
- package/dist/data/categories.d.ts +54 -0
- package/dist/data/categories.d.ts.map +1 -0
- package/dist/data/categories.js +97 -0
- package/dist/data/classification.d.ts +34 -0
- package/dist/data/classification.d.ts.map +1 -0
- package/dist/data/classification.js +28 -0
- package/dist/data/enums.d.ts +82 -0
- package/dist/data/enums.d.ts.map +1 -0
- package/dist/data/enums.js +70 -0
- package/dist/data/flag.d.ts +25 -0
- package/dist/data/flag.d.ts.map +1 -0
- package/dist/data/flag.js +27 -0
- package/dist/data/message.d.ts +81 -0
- package/dist/data/message.d.ts.map +1 -0
- package/dist/data/message.js +84 -0
- package/dist/data/tables.d.ts +160 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tables.js +44 -0
- package/dist/data/thread.d.ts +77 -0
- package/dist/data/thread.d.ts.map +1 -0
- package/dist/data/thread.js +65 -0
- package/dist/error/errors.d.ts +64 -0
- package/dist/error/errors.d.ts.map +1 -0
- package/dist/error/errors.js +82 -0
- package/dist/http/guards.d.ts +44 -0
- package/dist/http/guards.d.ts.map +1 -0
- package/dist/http/guards.js +53 -0
- package/dist/http/handlers.d.ts +89 -0
- package/dist/http/handlers.d.ts.map +1 -0
- package/dist/http/handlers.js +248 -0
- package/dist/http/resolve.d.ts +44 -0
- package/dist/http/resolve.d.ts.map +1 -0
- package/dist/http/resolve.js +72 -0
- package/dist/http/responses.d.ts +829 -0
- package/dist/http/responses.d.ts.map +1 -0
- package/dist/http/responses.js +303 -0
- package/dist/http/routes.d.ts +109 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +62 -0
- package/dist/http/schemas.d.ts +121 -0
- package/dist/http/schemas.d.ts.map +1 -0
- package/dist/http/schemas.js +89 -0
- package/dist/http/scopes.d.ts +56 -0
- package/dist/http/scopes.d.ts.map +1 -0
- package/dist/http/scopes.js +107 -0
- package/dist/http/views.d.ts +36 -0
- package/dist/http/views.d.ts.map +1 -0
- package/dist/http/views.js +144 -0
- package/dist/inbound/authenticity.d.ts +81 -0
- package/dist/inbound/authenticity.d.ts.map +1 -0
- package/dist/inbound/authenticity.js +61 -0
- package/dist/inbound/guard.d.ts +74 -0
- package/dist/inbound/guard.d.ts.map +1 -0
- package/dist/inbound/guard.js +78 -0
- package/dist/inbound/handler.d.ts +28 -0
- package/dist/inbound/handler.d.ts.map +1 -0
- package/dist/inbound/handler.js +82 -0
- package/dist/inbound/ingest.d.ts +104 -0
- package/dist/inbound/ingest.d.ts.map +1 -0
- package/dist/inbound/ingest.js +326 -0
- package/dist/inbound/recipient.d.ts +16 -0
- package/dist/inbound/recipient.d.ts.map +1 -0
- package/dist/inbound/recipient.js +46 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/link/sender.d.ts +160 -0
- package/dist/link/sender.d.ts.map +1 -0
- package/dist/link/sender.js +200 -0
- package/dist/migrations/0001_threads.d.ts +26 -0
- package/dist/migrations/0001_threads.d.ts.map +1 -0
- package/dist/migrations/0001_threads.js +103 -0
- package/dist/mime/address.d.ts +22 -0
- package/dist/mime/address.d.ts.map +1 -0
- package/dist/mime/address.js +28 -0
- package/dist/mime/parse.d.ts +96 -0
- package/dist/mime/parse.d.ts.map +1 -0
- package/dist/mime/parse.js +146 -0
- package/dist/mime/sanitize.d.ts +25 -0
- package/dist/mime/sanitize.d.ts.map +1 -0
- package/dist/mime/sanitize.js +212 -0
- package/dist/mime/threading.d.ts +79 -0
- package/dist/mime/threading.d.ts.map +1 -0
- package/dist/mime/threading.js +111 -0
- package/dist/mime/truncate.d.ts +17 -0
- package/dist/mime/truncate.d.ts.map +1 -0
- package/dist/mime/truncate.js +47 -0
- package/dist/provision/provisionSupport.d.ts +158 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -0
- package/dist/provision/provisionSupport.js +110 -0
- package/dist/provision/resolveSupportConfig.d.ts +45 -0
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -0
- package/dist/provision/resolveSupportConfig.js +22 -0
- package/dist/reply/send.d.ts +123 -0
- package/dist/reply/send.d.ts.map +1 -0
- package/dist/reply/send.js +153 -0
- package/dist/reply/snippets.d.ts +79 -0
- package/dist/reply/snippets.d.ts.map +1 -0
- package/dist/reply/snippets.js +135 -0
- package/dist/secret/registry.d.ts +18 -0
- package/dist/secret/registry.d.ts.map +1 -0
- package/dist/secret/registry.js +21 -0
- package/dist/seeds/example.d.ts +3 -0
- package/dist/seeds/example.d.ts.map +1 -0
- package/dist/seeds/example.js +350 -0
- package/dist/store/paging.d.ts +18 -0
- package/dist/store/paging.d.ts.map +1 -0
- package/dist/store/paging.js +20 -0
- package/dist/store/search.d.ts +67 -0
- package/dist/store/search.d.ts.map +1 -0
- package/dist/store/search.js +160 -0
- package/dist/store/searchIndex.d.ts +53 -0
- package/dist/store/searchIndex.d.ts.map +1 -0
- package/dist/store/searchIndex.js +66 -0
- package/dist/store/threads.d.ts +147 -0
- package/dist/store/threads.d.ts.map +1 -0
- package/dist/store/threads.js +197 -0
- package/dist/submission/encoding.d.ts +21 -0
- package/dist/submission/encoding.d.ts.map +1 -0
- package/dist/submission/encoding.js +55 -0
- package/dist/submission/guard.d.ts +71 -0
- package/dist/submission/guard.d.ts.map +1 -0
- package/dist/submission/guard.js +46 -0
- package/dist/submission/submit.d.ts +152 -0
- package/dist/submission/submit.d.ts.map +1 -0
- package/dist/submission/submit.js +319 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/workflows/classify.d.ts +63 -0
- package/dist/workflows/classify.d.ts.map +1 -0
- package/dist/workflows/classify.js +68 -0
- package/dist/workflows/retryPolicy.d.ts +39 -0
- package/dist/workflows/retryPolicy.d.ts.map +1 -0
- package/dist/workflows/retryPolicy.js +43 -0
- package/dist/workflows/specs.d.ts +43 -0
- package/dist/workflows/specs.d.ts.map +1 -0
- package/dist/workflows/specs.js +46 -0
- package/dist/workflows/worker.d.ts +48 -0
- package/dist/workflows/worker.d.ts.map +1 -0
- package/dist/workflows/worker.js +37 -0
- package/package.json +24 -15
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { resolveReplies } from "./reply/snippets.js";
|
|
2
|
+
import { SupportConfig, supportNeedsBucket } from "./config/config.js";
|
|
3
|
+
import { resolveCategories } from "./data/categories.js";
|
|
4
|
+
import { supportTables } from "./data/tables.js";
|
|
5
|
+
import { supportSecretsRegistry } from "./secret/registry.js";
|
|
6
|
+
import { supportWorkflows } from "./workflows/specs.js";
|
|
7
|
+
import { makeResolveDeps } from "./http/resolve.js";
|
|
8
|
+
import { supportAdminRoutes } from "./http/scopes.js";
|
|
9
|
+
import { registerSupportRoutes } from "./http/routes.js";
|
|
10
|
+
import { createSupportEmailHandler } from "./inbound/handler.js";
|
|
11
|
+
import { support_0001_threads } from "./migrations/0001_threads.js";
|
|
12
|
+
import { supportExampleSeed } from "./seeds/example.js";
|
|
13
|
+
import { PACKAGE_VERSION } from "./version.generated.js";
|
|
14
|
+
import { noopEmit } from "@pithy-sh/core/src/audit/recorder";
|
|
15
|
+
import { defineCapability } from "@pithy-sh/core/src/capability/capability";
|
|
16
|
+
import { workflowBindings } from "@pithy-sh/core/src/workflow/bindings";
|
|
17
|
+
import { isEmailCapability } from "@pithy-sh/email/src/capability";
|
|
18
|
+
//#region src/capability.ts
|
|
19
|
+
/**
|
|
20
|
+
* Sort order of the support migrations within the app database, relative to other capabilities.
|
|
21
|
+
* Unique per database; the registry composes keys like `1200_support_0001_threads`.
|
|
22
|
+
*
|
|
23
|
+
* Taken from `NEXT_FREE_ORDER` in `packages/cli/src/migrations/orders.test.ts` at implementation
|
|
24
|
+
* time, as the procedure in that file requires — never picked by grepping, which is how two pairs of
|
|
25
|
+
* capabilities once collided in a range that was 99% empty. Stable forever: renumbering would rename
|
|
26
|
+
* the composed keys, and Kysely would then read applied migrations as unapplied and re-run them.
|
|
27
|
+
*/
|
|
28
|
+
const SUPPORT_MIGRATION_ORDER = 1200;
|
|
29
|
+
/**
|
|
30
|
+
* What a browser may know about this project's support — the `virtual:pithy/support` module.
|
|
31
|
+
*
|
|
32
|
+
* **One route on this capability is called by a browser**, and that is the whole reason this exists:
|
|
33
|
+
* `POST {basePath}/feedback`, with the two reads beside it. Everything else answers to a control-plane
|
|
34
|
+
* credential a management client holds, and a management client reads the manifest. So the list is
|
|
35
|
+
* `basePath` and the bounds the compose form must hold somebody to before it lets them press Send —
|
|
36
|
+
* a form that stops at 200 characters because the handler refuses at 200 characters is a better form
|
|
37
|
+
* than one that finds out afterwards.
|
|
38
|
+
*
|
|
39
|
+
* **`{ enabled: false }` when the submission channel is off**, and that is not the same statement as
|
|
40
|
+
* "support is not composed" only in a sentence nobody writes. `registerSupportRoutes` does not mount
|
|
41
|
+
* the feedback routes when `submission.enabled` is false — they answer 404, not 403 — so a browser
|
|
42
|
+
* then has nothing on this capability to call and no use for a path. A screen branches on `enabled`
|
|
43
|
+
* rather than guarding, exactly as it does for a payments catalog with nothing in it.
|
|
44
|
+
*
|
|
45
|
+
* **What deliberately does not cross, and why each one was considered.**
|
|
46
|
+
* - **The taxonomy.** A category's value is the instruction a model reads and it lands in the prompt
|
|
47
|
+
* verbatim — prompt input written for a classifier, not copy for a chooser. An adopter's UI wants
|
|
48
|
+
* its own words either way, and the descriptions are the closest thing here to authored internals.
|
|
49
|
+
* - **`inboundAddresses` and `reply.replyToAddress`.** The inbox's own mail addresses. A browser never
|
|
50
|
+
* writes to them, and publishing an address into every bundle is a favor to nobody but a spammer.
|
|
51
|
+
* - **`reply.snippets`.** Canned copy an operator picks and edits in a dashboard. Staff-side, and the
|
|
52
|
+
* dashboard reads it through the control-plane route that already serves it.
|
|
53
|
+
* - **`submission.maxPerAccountPerHour`.** A rate a client cannot pre-enforce honestly — the count
|
|
54
|
+
* lives in D1 and the server's refusal is the only truth about it — so projecting it would publish
|
|
55
|
+
* an abuse budget in exchange for a number no form can act on.
|
|
56
|
+
* - **`ai`, `guard`, `attachments`, `search`.** The model and its prompt budget, the mail path's size
|
|
57
|
+
* and rate bounds, what happens to mail attachments, and how search is indexed. Each describes the
|
|
58
|
+
* deployment or a surface a browser never touches. The mail path's `attachments` block in
|
|
59
|
+
* particular is *not* the submission's: `config.ts` says at length why the two are stated
|
|
60
|
+
* separately, and inheriting one for the other here would undo that in the one place it reaches a
|
|
61
|
+
* client.
|
|
62
|
+
*
|
|
63
|
+
* There is no secret in reach of this closure to begin with — the R2 credential bundle lives in the
|
|
64
|
+
* secrets store behind `supportSecretsRegistry` and is never in config — so what discipline covers is
|
|
65
|
+
* the list above. `capability.test.ts` locks the key set by hand and sweeps the serialized result.
|
|
66
|
+
*
|
|
67
|
+
* `null` for attachments that are off, never `undefined`: the projection is inlined into a bundle with
|
|
68
|
+
* `JSON.stringify`, which drops an undefined value and leaves the screen reading a key that is simply
|
|
69
|
+
* absent. Null is also what makes "render no file picker" one check rather than three absences.
|
|
70
|
+
*
|
|
71
|
+
* The return type is {@link SupportClientProjection} — **declared, not inferred**. `ClientProjection` is
|
|
72
|
+
* `{ enabled: boolean }` plus a JSON catchall, which accepts anything this function could return. The
|
|
73
|
+
* declared type is what makes a dropped field a compile error here rather than a browser's problem, and
|
|
74
|
+
* what stops the list above widening by accident when `SupportConfig` grows.
|
|
75
|
+
*/
|
|
76
|
+
function clientProjection(config, basePath) {
|
|
77
|
+
const submission = config.submission;
|
|
78
|
+
if (!submission.enabled) return { enabled: false };
|
|
79
|
+
const attachments = submission.attachments;
|
|
80
|
+
return {
|
|
81
|
+
enabled: true,
|
|
82
|
+
basePath,
|
|
83
|
+
submission: {
|
|
84
|
+
maxSubjectChars: submission.maxSubjectChars,
|
|
85
|
+
maxBodyChars: submission.maxBodyChars,
|
|
86
|
+
attachments: attachments.enabled ? {
|
|
87
|
+
maxCount: attachments.maxCount,
|
|
88
|
+
maxBytes: attachments.maxBytes,
|
|
89
|
+
allowedContentTypes: [...attachments.allowedContentTypes]
|
|
90
|
+
} : null
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The support capability — an inbound support inbox in the adopter's own D1, classified on their own
|
|
96
|
+
* Workers AI binding.
|
|
97
|
+
*
|
|
98
|
+
* It contributes five tables (and optionally a full-text index) to the app `DB`, an `email()` handler
|
|
99
|
+
* that claims the configured addresses out of the Worker's single inbound entry, the classification
|
|
100
|
+
* Workflow that handler dispatches to, and a control-plane admin surface for a dashboard to read and
|
|
101
|
+
* act on.
|
|
102
|
+
*
|
|
103
|
+
* **Nothing here is gated by tier.** Every route is reachable with a control-plane credential the
|
|
104
|
+
* adopter issued, whatever anyone pays; a hosted dashboard gates its own UI and nothing else.
|
|
105
|
+
* Crippling MIT code would be both wrong and futile.
|
|
106
|
+
*
|
|
107
|
+
* Attachments presign through `@pithy-sh/storage`'s `ObjectStore` against `SUPPORT_BUCKET` under a
|
|
108
|
+
* credential name support declares, so `secrets` must be composed. The admin routes need the
|
|
109
|
+
* `control-plane` seam; without it every one denies, which is the correct failure for an inbox
|
|
110
|
+
* holding other people's correspondence. `@pithy-sh/auth` and `@pithy-sh/payments` are reached by
|
|
111
|
+
* guarded dynamic import for the sender link, so both stay genuinely optional.
|
|
112
|
+
*/
|
|
113
|
+
function support(options = {}) {
|
|
114
|
+
const { basePath, ...configInput } = options;
|
|
115
|
+
const resolved = SupportConfig.parse(configInput);
|
|
116
|
+
const categories = resolveCategories(resolved.categories);
|
|
117
|
+
const snippets = resolveReplies(resolved.reply.snippets);
|
|
118
|
+
const mountPath = basePath ?? "/support";
|
|
119
|
+
const wiring = {
|
|
120
|
+
config: resolved,
|
|
121
|
+
categories,
|
|
122
|
+
snippets,
|
|
123
|
+
enqueueEmail: void 0,
|
|
124
|
+
emit: noopEmit
|
|
125
|
+
};
|
|
126
|
+
const migrations = { "0001_threads": support_0001_threads };
|
|
127
|
+
const requiredBindings = [
|
|
128
|
+
{
|
|
129
|
+
type: "d1",
|
|
130
|
+
name: "DB"
|
|
131
|
+
},
|
|
132
|
+
...supportNeedsBucket(resolved) ? [{
|
|
133
|
+
type: "r2",
|
|
134
|
+
name: "SUPPORT_BUCKET",
|
|
135
|
+
optional: true
|
|
136
|
+
}] : [],
|
|
137
|
+
...workflowBindings(supportWorkflows)
|
|
138
|
+
];
|
|
139
|
+
const capability = defineCapability({
|
|
140
|
+
name: "support",
|
|
141
|
+
version: PACKAGE_VERSION,
|
|
142
|
+
dependsOn: ["secrets"],
|
|
143
|
+
secretRegistry: supportSecretsRegistry,
|
|
144
|
+
workflows: supportWorkflows,
|
|
145
|
+
requiredBindings,
|
|
146
|
+
ciPermissions: ["email:routing"],
|
|
147
|
+
databases: { app: {
|
|
148
|
+
binding: "DB",
|
|
149
|
+
tables: supportTables,
|
|
150
|
+
migrationOrder: SUPPORT_MIGRATION_ORDER,
|
|
151
|
+
migrations
|
|
152
|
+
} },
|
|
153
|
+
/**
|
|
154
|
+
* Bind the optional email seam once, at startup.
|
|
155
|
+
*
|
|
156
|
+
* Deliberately not a hard `dependsOn`. Auth *requires* email — a magic link that cannot be sent
|
|
157
|
+
* is a broken sign-in — but a support inbox that receives, threads, and classifies without being
|
|
158
|
+
* able to answer is a coherent and useful thing, so a missing email capability degrades one
|
|
159
|
+
* route instead of refusing to boot.
|
|
160
|
+
*/
|
|
161
|
+
compose: ({ capabilities }) => {
|
|
162
|
+
const email = capabilities.find(isEmailCapability);
|
|
163
|
+
wiring.enqueueEmail = email?.enqueue;
|
|
164
|
+
},
|
|
165
|
+
client: () => clientProjection(resolved, mountPath),
|
|
166
|
+
adminRoutes: supportAdminRoutes(mountPath),
|
|
167
|
+
routes: registerSupportRoutes({
|
|
168
|
+
basePath: mountPath,
|
|
169
|
+
submission: resolved.submission.enabled,
|
|
170
|
+
resolveDeps: makeResolveDeps(wiring)
|
|
171
|
+
}),
|
|
172
|
+
email: createSupportEmailHandler(wiring),
|
|
173
|
+
seeds: [supportExampleSeed]
|
|
174
|
+
});
|
|
175
|
+
return Object.assign(capability, {
|
|
176
|
+
supportConfig: resolved,
|
|
177
|
+
categories,
|
|
178
|
+
snippets,
|
|
179
|
+
basePath: mountPath
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Whether a capability is the support capability — carries its resolved config and effective taxonomy. */
|
|
183
|
+
function isSupportCapability(capability) {
|
|
184
|
+
return capability.name === "support" && "supportConfig" in capability;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { SUPPORT_MIGRATION_ORDER, isSupportCapability, support };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a browser may know about this project's support inbox — the shape of `virtual:pithy/support`.
|
|
3
|
+
*
|
|
4
|
+
* **This declaration is the contract, and the projection is checked against it.** It is written here
|
|
5
|
+
* rather than inferred from the closure that builds it, and that is the whole point: an inferred type
|
|
6
|
+
* follows whatever the producer last happened to say, so a projection that dropped `maxBodyChars`, or
|
|
7
|
+
* that started passing the mail path's attachment bounds through because `SupportConfig` moved a field,
|
|
8
|
+
* would take the type with it and nothing would go red. Declared, the arrow is the thing that has to
|
|
9
|
+
* change — and every widening of what a browser sees is a decision made here, on purpose.
|
|
10
|
+
*
|
|
11
|
+
* The list is short on purpose and `clientProjection`'s own comment says at length what stays behind:
|
|
12
|
+
* the taxonomy, the inbound addresses, the reply snippets, the per-account rate, and the whole `ai`,
|
|
13
|
+
* `guard`, mail-`attachments` and `search` blocks.
|
|
14
|
+
*
|
|
15
|
+
* **This is the only statement of the shape.** `@pithy-sh/ui-react`'s `templates/client-env.d.ts` — the
|
|
16
|
+
* ambient declaration `pithy ui add react` copies into an adopter's Worker — is generated from this type
|
|
17
|
+
* by `@pithy-sh/vite`'s `clientEnvDeclaration.ts` (#398). The unions and the per-field doc comments below
|
|
18
|
+
* are emitted verbatim, so what is written here is what a screen author reads.
|
|
19
|
+
*/
|
|
20
|
+
export type SupportClientProjection = {
|
|
21
|
+
/**
|
|
22
|
+
* Support is not composed, or is not serving the in-app submission routes — the only ones a
|
|
23
|
+
* browser calls. A screen branches rather than rendering a compose form nothing will accept.
|
|
24
|
+
*/
|
|
25
|
+
enabled: false;
|
|
26
|
+
} | {
|
|
27
|
+
/** Support is composed AND serving the in-app submission routes. */
|
|
28
|
+
enabled: true;
|
|
29
|
+
/** Where the support routes mount, e.g. `/support`. `POST {basePath}/feedback` writes in. */
|
|
30
|
+
basePath: string;
|
|
31
|
+
/**
|
|
32
|
+
* What one submission may carry. Hold a compose form to these so the handler does not have to
|
|
33
|
+
* refuse after somebody pressed Send. The taxonomy is not here: a category's text is the
|
|
34
|
+
* instruction a classifier reads, not copy for a chooser.
|
|
35
|
+
*/
|
|
36
|
+
submission: {
|
|
37
|
+
/** The longest subject accepted. It becomes the thread's name in the inbox. */
|
|
38
|
+
maxSubjectChars: number;
|
|
39
|
+
/** The longest report body accepted. */
|
|
40
|
+
maxBodyChars: number;
|
|
41
|
+
/**
|
|
42
|
+
* What an upload control may offer, or null when attachments are off and it renders none.
|
|
43
|
+
* Null rather than absent: the projection is inlined with `JSON.stringify`, which drops an
|
|
44
|
+
* undefined value and leaves a screen reading a key that is simply gone.
|
|
45
|
+
*/
|
|
46
|
+
attachments: {
|
|
47
|
+
/** How many files one submission may carry. */
|
|
48
|
+
maxCount: number;
|
|
49
|
+
/** The largest single file, measured on the decoded bytes. */
|
|
50
|
+
maxBytes: number;
|
|
51
|
+
/** The exact MIME types accepted — an allowlist, and the `accept` a file input wants. */
|
|
52
|
+
allowedContentTypes: string[];
|
|
53
|
+
} | null;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/client/projection.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,uBAAuB,GAC/B;IACE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,oEAAoE;IACpE,OAAO,EAAE,IAAI,CAAC;IACd,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,EAAE;QACV,+EAA+E;QAC/E,eAAe,EAAE,MAAM,CAAC;QACxB,wCAAwC;QACxC,YAAY,EAAE,MAAM,CAAC;QACrB;;;;WAIG;QACH,WAAW,EAAE;YACX,+CAA+C;YAC/C,QAAQ,EAAE,MAAM,CAAC;YACjB,8DAA8D;YAC9D,QAAQ,EAAE,MAAM,CAAC;YACjB,yFAAyF;YACzF,mBAAmB,EAAE,MAAM,EAAE,CAAC;SAC/B,GAAG,IAAI,CAAC;KACV,CAAC;CACH,CAAC"}
|
|
File without changes
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The support capability's configuration — the thin, user-owned surface in `pithy.config.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The one field with no sensible default is `inboundAddresses`, and the reason is worth stating
|
|
6
|
+
* where an adopter will read it: **Cloudflare Email Routing takes over a zone's MX.** An address on
|
|
7
|
+
* the apex would move the adopter's real mail off their existing provider, so the address is theirs
|
|
8
|
+
* to choose deliberately — almost always on a subdomain (`support@help.theirdomain.com`) — and this
|
|
9
|
+
* capability will not guess one. Until it is set the inbox is inert: every message is ignored, and
|
|
10
|
+
* the handler says so once in the log rather than quietly storing mail from an address nobody
|
|
11
|
+
* configured.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The default Workers AI model for classification.
|
|
15
|
+
*
|
|
16
|
+
* An instruct model rather than a classifier: the taxonomy is federated, so the valid label set is
|
|
17
|
+
* not known until an adopter composes the capability, and a fine-tuned classifier cannot be given new
|
|
18
|
+
* classes in a config file. Small on purpose — this runs once per inbound message on the adopter's
|
|
19
|
+
* own bill, and the judgment is "which of these eight sentences fits", not a reasoning task.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_CLASSIFY_MODEL = "@cf/meta/llama-3.1-8b-instruct";
|
|
22
|
+
/** How the inbound message is classified, and by what. */
|
|
23
|
+
export declare const SupportAiConfig: z.ZodObject<{
|
|
24
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
25
|
+
model: z.ZodDefault<z.ZodString>;
|
|
26
|
+
maxChars: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
temperature: z.ZodDefault<z.ZodNumber>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type SupportAiConfig = z.output<typeof SupportAiConfig>;
|
|
30
|
+
/** What happens to files that arrive attached to a message. */
|
|
31
|
+
export declare const SupportAttachmentsConfig: z.ZodObject<{
|
|
32
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
33
|
+
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
maxCount: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
retainRaw: z.ZodDefault<z.ZodBoolean>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
export type SupportAttachmentsConfig = z.output<typeof SupportAttachmentsConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* The spam and volume guard.
|
|
40
|
+
*
|
|
41
|
+
* A public address is a public write endpoint into the adopter's D1, and that is the honest way to
|
|
42
|
+
* think about it. These bounds are what stop a mail flood from being a storage bill.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SupportGuardConfig: z.ZodObject<{
|
|
45
|
+
maxRawBytes: z.ZodDefault<z.ZodNumber>;
|
|
46
|
+
maxPerSenderPerHour: z.ZodDefault<z.ZodNumber>;
|
|
47
|
+
maxPerHour: z.ZodDefault<z.ZodNumber>;
|
|
48
|
+
trustAuthenticationResults: z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
authservId: z.ZodOptional<z.ZodString>;
|
|
50
|
+
archiveSpam: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export type SupportGuardConfig = z.output<typeof SupportGuardConfig>;
|
|
53
|
+
/**
|
|
54
|
+
* What a signed-in user may attach to an in-app submission.
|
|
55
|
+
*
|
|
56
|
+
* **Stated here rather than inherited from `attachments`, and that is the point.** The mail path's
|
|
57
|
+
* bounds were written for parts of a MIME message somebody sent to a public address: they cap size and
|
|
58
|
+
* count, and they say nothing at all about type, because there is no useful type restriction on mail —
|
|
59
|
+
* refusing a `.docx` a customer attached to a bug report would lose the report. A direct upload from a
|
|
60
|
+
* browser is a different surface with a different answer: the client is authenticated but untrusted,
|
|
61
|
+
* the useful payload is a screenshot, and an allowlist is both possible and worth having. Inheriting
|
|
62
|
+
* the email numbers would have meant a 10 MB any-type upload endpoint arriving as a side effect of a
|
|
63
|
+
* setting an adopter tuned for their inbox.
|
|
64
|
+
*
|
|
65
|
+
* Bytes are stored exactly as the mail path stores them — server-derived opaque key,
|
|
66
|
+
* `application/octet-stream` on the object whatever was declared — so nothing here re-opens the
|
|
67
|
+
* stored-XSS question `attachment/store.ts` already closed.
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* The hard ceiling on how many attachments one submission may declare, whatever
|
|
71
|
+
* `submission.attachments.maxCount` is set to.
|
|
72
|
+
*
|
|
73
|
+
* On the schema as well as in the handler because the two refuse at different moments and the earlier
|
|
74
|
+
* one is free: the validator rejects a thousand-element array before a handler runs, without reading
|
|
75
|
+
* the resolved config. The configured bound is what an adopter tunes; this is what is true regardless.
|
|
76
|
+
*/
|
|
77
|
+
export declare const MAX_SUBMISSION_ATTACHMENTS = 10;
|
|
78
|
+
export declare const SupportSubmissionAttachmentsConfig: z.ZodObject<{
|
|
79
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
80
|
+
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
81
|
+
maxCount: z.ZodDefault<z.ZodNumber>;
|
|
82
|
+
allowedContentTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
export type SupportSubmissionAttachmentsConfig = z.output<typeof SupportSubmissionAttachmentsConfig>;
|
|
85
|
+
/**
|
|
86
|
+
* The ceilings a configured submission bound may not exceed — the route's own schema is written to
|
|
87
|
+
* these, so a payload beyond one is refused by the validator before a handler runs.
|
|
88
|
+
*
|
|
89
|
+
* Two layers, and both earn their place. The **config** bound is the adopter's number and produces the
|
|
90
|
+
* message their user reads; the **schema** ceiling is what stops a megabyte of text reaching a handler
|
|
91
|
+
* at all, and it cannot be config-derived because a request schema is built once at module load while
|
|
92
|
+
* config is resolved per project. Bounding the config field by the same constant is what keeps them
|
|
93
|
+
* from disagreeing: a setting above the ceiling would be a limit the route silently refused to honor.
|
|
94
|
+
*/
|
|
95
|
+
export declare const MAX_SUBMISSION_SUBJECT_CHARS = 500;
|
|
96
|
+
/** The hard ceiling on a submission body, whatever `submission.maxBodyChars` is set to. */
|
|
97
|
+
export declare const MAX_SUBMISSION_BODY_CHARS = 100000;
|
|
98
|
+
/**
|
|
99
|
+
* The in-app submission channel: a signed-in user of the adopter's own app opening a support thread
|
|
100
|
+
* without leaving it.
|
|
101
|
+
*
|
|
102
|
+
* **The hardest problem in the mail path does not exist here.** `inbound/authenticity.ts` spends two
|
|
103
|
+
* hundred lines earning a customer link from a `From:` header that anybody can write; a submission
|
|
104
|
+
* arrives on a request `requireAuth()` already proved, so the account is the identity rather than a
|
|
105
|
+
* guess about it. Everything below is therefore a bound on *volume and shape*, never on identity.
|
|
106
|
+
*
|
|
107
|
+
* Bounded separately from `guard` for the same reason it is counted separately: that block exists
|
|
108
|
+
* because a public address is a public write endpoint, and this surface is neither public nor
|
|
109
|
+
* anonymous. Abuse here is attributable to an account and revocable with it, which is a materially
|
|
110
|
+
* different threat model and deserves its own numbers rather than a share of somebody else's.
|
|
111
|
+
*/
|
|
112
|
+
export declare const SupportSubmissionConfig: z.ZodObject<{
|
|
113
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
114
|
+
maxSubjectChars: z.ZodDefault<z.ZodNumber>;
|
|
115
|
+
maxBodyChars: z.ZodDefault<z.ZodNumber>;
|
|
116
|
+
maxPerAccountPerHour: z.ZodDefault<z.ZodNumber>;
|
|
117
|
+
attachments: z.ZodPrefault<z.ZodObject<{
|
|
118
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
+
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
120
|
+
maxCount: z.ZodDefault<z.ZodNumber>;
|
|
121
|
+
allowedContentTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
122
|
+
}, z.core.$strip>>;
|
|
123
|
+
}, z.core.$strip>;
|
|
124
|
+
export type SupportSubmissionConfig = z.output<typeof SupportSubmissionConfig>;
|
|
125
|
+
/** How a reply leaves the building. */
|
|
126
|
+
export declare const SupportReplyConfig: z.ZodObject<{
|
|
127
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
128
|
+
replyToAddress: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
129
|
+
deliverInApp: z.ZodDefault<z.ZodBoolean>;
|
|
130
|
+
snippets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
131
|
+
label: z.ZodString;
|
|
132
|
+
category: z.ZodOptional<z.ZodString>;
|
|
133
|
+
body: z.ZodString;
|
|
134
|
+
}, z.core.$strip>>>;
|
|
135
|
+
}, z.core.$strip>;
|
|
136
|
+
export type SupportReplyConfig = z.output<typeof SupportReplyConfig>;
|
|
137
|
+
/**
|
|
138
|
+
* How text search is answered.
|
|
139
|
+
*
|
|
140
|
+
* The one setting here has a cost an adopter must opt into knowingly, which is why it is a setting
|
|
141
|
+
* at all rather than a decision this package made for them.
|
|
142
|
+
*/
|
|
143
|
+
export declare const SupportSearchConfig: z.ZodObject<{
|
|
144
|
+
fts: z.ZodDefault<z.ZodBoolean>;
|
|
145
|
+
}, z.core.$strip>;
|
|
146
|
+
export type SupportSearchConfig = z.output<typeof SupportSearchConfig>;
|
|
147
|
+
/** The full support configuration. */
|
|
148
|
+
export declare const SupportConfig: z.ZodObject<{
|
|
149
|
+
inboundAddresses: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
150
|
+
categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
151
|
+
ai: z.ZodPrefault<z.ZodObject<{
|
|
152
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
153
|
+
model: z.ZodDefault<z.ZodString>;
|
|
154
|
+
maxChars: z.ZodDefault<z.ZodNumber>;
|
|
155
|
+
temperature: z.ZodDefault<z.ZodNumber>;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
attachments: z.ZodPrefault<z.ZodObject<{
|
|
158
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
159
|
+
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
160
|
+
maxCount: z.ZodDefault<z.ZodNumber>;
|
|
161
|
+
retainRaw: z.ZodDefault<z.ZodBoolean>;
|
|
162
|
+
}, z.core.$strip>>;
|
|
163
|
+
guard: z.ZodPrefault<z.ZodObject<{
|
|
164
|
+
maxRawBytes: z.ZodDefault<z.ZodNumber>;
|
|
165
|
+
maxPerSenderPerHour: z.ZodDefault<z.ZodNumber>;
|
|
166
|
+
maxPerHour: z.ZodDefault<z.ZodNumber>;
|
|
167
|
+
trustAuthenticationResults: z.ZodDefault<z.ZodBoolean>;
|
|
168
|
+
authservId: z.ZodOptional<z.ZodString>;
|
|
169
|
+
archiveSpam: z.ZodDefault<z.ZodBoolean>;
|
|
170
|
+
}, z.core.$strip>>;
|
|
171
|
+
submission: z.ZodPrefault<z.ZodObject<{
|
|
172
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
173
|
+
maxSubjectChars: z.ZodDefault<z.ZodNumber>;
|
|
174
|
+
maxBodyChars: z.ZodDefault<z.ZodNumber>;
|
|
175
|
+
maxPerAccountPerHour: z.ZodDefault<z.ZodNumber>;
|
|
176
|
+
attachments: z.ZodPrefault<z.ZodObject<{
|
|
177
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
178
|
+
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
179
|
+
maxCount: z.ZodDefault<z.ZodNumber>;
|
|
180
|
+
allowedContentTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
181
|
+
}, z.core.$strip>>;
|
|
182
|
+
}, z.core.$strip>>;
|
|
183
|
+
reply: z.ZodPrefault<z.ZodObject<{
|
|
184
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
185
|
+
replyToAddress: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
|
|
186
|
+
deliverInApp: z.ZodDefault<z.ZodBoolean>;
|
|
187
|
+
snippets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
188
|
+
label: z.ZodString;
|
|
189
|
+
category: z.ZodOptional<z.ZodString>;
|
|
190
|
+
body: z.ZodString;
|
|
191
|
+
}, z.core.$strip>>>;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
search: z.ZodPrefault<z.ZodObject<{
|
|
194
|
+
fts: z.ZodDefault<z.ZodBoolean>;
|
|
195
|
+
}, z.core.$strip>>;
|
|
196
|
+
}, z.core.$strip>;
|
|
197
|
+
export type SupportConfig = z.output<typeof SupportConfig>;
|
|
198
|
+
export type SupportConfigInput = z.input<typeof SupportConfig>;
|
|
199
|
+
/**
|
|
200
|
+
* Would this configuration ever put a byte in `SUPPORT_BUCKET`?
|
|
201
|
+
*
|
|
202
|
+
* **The one place the question is answered, because three independent settings answer it and each has
|
|
203
|
+
* its own writer.** `attachments.enabled` gates stored mail attachments (`inbound/ingest.ts:200`),
|
|
204
|
+
* `attachments.retainRaw` gates the raw MIME copy (`inbound/ingest.ts:346`), and
|
|
205
|
+
* `submission.attachments.enabled` gates an in-app submission's files (`submission/submit.ts:268`).
|
|
206
|
+
* All three default `true`, so the ordinary project is unaffected by this predicate existing.
|
|
207
|
+
*
|
|
208
|
+
* Two callers, and they must agree: `capability.ts` declares the binding only when this is true, and
|
|
209
|
+
* `pithy support provision` creates the bucket only when this is true. They disagreed before #440 —
|
|
210
|
+
* the provisioner asked about `enabled` and `retainRaw` and never about submissions, so a project that
|
|
211
|
+
* wanted uploads but no mail attachments got a binding pointing at a bucket nothing had created, and
|
|
212
|
+
* every submitted file was dropped with a warning. One predicate is what makes that unrepresentable.
|
|
213
|
+
*/
|
|
214
|
+
export declare function supportNeedsBucket(config: SupportConfig): boolean;
|
|
215
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;GAUG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,mCAAmC,CAAC;AAEvE,0DAA0D;AAC1D,eAAO,MAAM,eAAe;;;;;iBA+BkF,CAAC;AAC/G,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE/D,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB;;;;;iBA6BkF,CAAC;AACxH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB;;;;;;;iBA8C8D,CAAC;AAC9F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;;;;;;;;;;;GAeG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,eAAO,MAAM,kCAAkC;;;;;iBAkC5C,CAAC;AACJ,MAAM,MAAM,kCAAkC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAErG;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAChD,2FAA2F;AAC3F,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,uBAAuB;;;;;;;;;;;iBAwCjC,CAAC;AACJ,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE/E,uCAAuC;AACvC,eAAO,MAAM,kBAAkB;;;;;;;;;iBAmC5B,CAAC;AACJ,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;iBAS8D,CAAC;AAC/F,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,sCAAsC;AACtC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAEjE"}
|