@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,48 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { workflowKey } from "@pithy-sh/core/src/workflow/naming";
|
|
5
|
+
//#region src/workflows/specs.ts
|
|
6
|
+
/**
|
|
7
|
+
* The one durable job support owns, declared once.
|
|
8
|
+
*
|
|
9
|
+
* **Classification is a Workflow, not part of the `email()` handler**, and that is a hard constraint
|
|
10
|
+
* rather than a preference. An inbound handler runs under a tight CPU budget; a model call is
|
|
11
|
+
* hundreds of milliseconds of wall time it cannot spend, and a model that is slow or briefly
|
|
12
|
+
* unavailable would take the *persistence* of the message down with it. So the handler does the
|
|
13
|
+
* cheap, must-not-fail half — parse, bound, store — and hands off. A classification that fails
|
|
14
|
+
* retries against a message that is already safely in D1.
|
|
15
|
+
*
|
|
16
|
+
* `optional: true`: the Workflow lives in the prebuilt support worker that `pithy support provision`
|
|
17
|
+
* deploys, so a project that has not provisioned must still boot and still receive mail. An absent
|
|
18
|
+
* binding degrades to a logged skip and a thread that stays `uncategorized` — which is exactly the
|
|
19
|
+
* state a classification is allowed to be in.
|
|
20
|
+
*/
|
|
21
|
+
/** The capability name — the first segment of the dispatch key and of every deployed workflow name. */
|
|
22
|
+
const SUPPORT_CAPABILITY = "support";
|
|
23
|
+
/** Which message to classify. */
|
|
24
|
+
const SupportClassifyParams = z.object({ messageId: z.string().min(1).describe("The `pithy_support_messages.id` to classify. The Workflow reads that message, asks the model about it, appends a classification row, and denormalizes the answer onto its thread.") }).describe("The instance parameters of the classification Workflow — the one message it runs against.");
|
|
25
|
+
/** Support's durable jobs, keyed by job name. The key is the second segment of the `support/<job>` dispatch key. */
|
|
26
|
+
const supportWorkflows = { classify: {
|
|
27
|
+
binding: "SUPPORT_CLASSIFY",
|
|
28
|
+
className: "SupportClassifyWorkflow",
|
|
29
|
+
params: SupportClassifyParams,
|
|
30
|
+
optional: true
|
|
31
|
+
} };
|
|
32
|
+
/**
|
|
33
|
+
* Support's jobs as a dispatch registry, keyed `support/<job>`. Built here rather than through
|
|
34
|
+
* `composeWorkflows` because the inbound handler dispatches from inside the capability, before any
|
|
35
|
+
* project-wide registry exists — and the key format comes from core's {@link workflowKey} either
|
|
36
|
+
* way, so the two cannot drift.
|
|
37
|
+
*/
|
|
38
|
+
const supportWorkflowRegistry = Object.fromEntries(Object.entries(supportWorkflows).map(([job, spec]) => {
|
|
39
|
+
const key = workflowKey(SUPPORT_CAPABILITY, job);
|
|
40
|
+
return [key, {
|
|
41
|
+
key,
|
|
42
|
+
capability: SUPPORT_CAPABILITY,
|
|
43
|
+
job,
|
|
44
|
+
spec
|
|
45
|
+
}];
|
|
46
|
+
}));
|
|
47
|
+
//#endregion
|
|
48
|
+
export { SUPPORT_CAPABILITY, SupportClassifyParams, supportWorkflowRegistry, supportWorkflows };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
|
|
4
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
5
|
+
import type { SupportAi } from "../ai/classify";
|
|
6
|
+
/**
|
|
7
|
+
* The prebuilt support worker. `pithy support provision` deploys one per environment; the adopter
|
|
8
|
+
* authors no code for it. It hosts the classification Workflow — a thin durable shell around the
|
|
9
|
+
* tested orchestration in `classify.ts`. The app worker receives the mail, stores it, and starts an
|
|
10
|
+
* instance.
|
|
11
|
+
*
|
|
12
|
+
* This module imports `cloudflare:workers`, so it runs only in the Workers runtime and is excluded
|
|
13
|
+
* from the node `.describe()` meta-test.
|
|
14
|
+
*
|
|
15
|
+
* **The effective taxonomy arrives as config, not as code.** `SUPPORT_CONFIG` carries the adopter's
|
|
16
|
+
* own categories, so their `tournament_dispute` reaches the prompt without this worker importing
|
|
17
|
+
* anything of theirs — which it could not do anyway, since it is deployed from this package.
|
|
18
|
+
*
|
|
19
|
+
* **The default export is what makes this an ES module** (#426). It exports one Workflow class and has
|
|
20
|
+
* no cron, so until now it exported no default — and wrangler infers a worker's module format from
|
|
21
|
+
* exactly that, so the build read it as a service worker and refused `cloudflare:workers` outright.
|
|
22
|
+
* The host did not build, `pithy dev` carried on past it, and classification silently never ran. The
|
|
23
|
+
* refusal it exports is the honest body for a host with no request surface; see
|
|
24
|
+
* `@pithy-sh/core/src/workflow/hostEntry`.
|
|
25
|
+
*/
|
|
26
|
+
/** The support worker's env: the app database, the AI binding, and the serialized config. */
|
|
27
|
+
export interface SupportWorkerEnv {
|
|
28
|
+
/** The app database the support tables live in. */
|
|
29
|
+
DB: D1Database;
|
|
30
|
+
/** The Workers AI binding. */
|
|
31
|
+
AI: SupportAi;
|
|
32
|
+
/** The resolved support config as a JSON string, filled at provision. */
|
|
33
|
+
SUPPORT_CONFIG?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Classify one stored message and write the result. */
|
|
36
|
+
export declare class SupportClassifyWorkflow extends WorkflowEntrypoint<SupportWorkerEnv, {
|
|
37
|
+
messageId: string;
|
|
38
|
+
}> {
|
|
39
|
+
run(event: WorkflowEvent<{
|
|
40
|
+
messageId: string;
|
|
41
|
+
}>, step: WorkflowStep): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The module's default export, and therefore its format. See `hostEntry` for why a Workflow host needs
|
|
45
|
+
* one at all, and why this one refuses rather than being empty: nothing reaches this worker over HTTP —
|
|
46
|
+
* the app worker stores the message and starts an instance on the `SUPPORT_CLASSIFY` binding.
|
|
47
|
+
*/
|
|
48
|
+
declare const _default: import("@pithy-sh/core/src/workflow/hostEntry").WorkflowHostEntry;
|
|
49
|
+
export default _default;
|
|
50
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/workflows/worker.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQhD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,EAAE,EAAE,UAAU,CAAC;IACf,8BAA8B;IAC9B,EAAE,EAAE,SAAS,CAAC;IACd,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wDAAwD;AACxD,qBAAa,uBAAwB,SAAQ,kBAAkB,CAAC,gBAAgB,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;IACvF,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBjG;CACF;AAED;;;;GAIG"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { SupportConfig } from "../config/config.js";
|
|
4
|
+
import { resolveCategories } from "../data/categories.js";
|
|
5
|
+
import { supportDatabase } from "../data/tables.js";
|
|
6
|
+
import { SUPPORT_CAPABILITY } from "./specs.js";
|
|
7
|
+
import { runClassification } from "./classify.js";
|
|
8
|
+
import { supportWorkflowRetry } from "./retryPolicy.js";
|
|
9
|
+
import { WorkflowEntrypoint } from "cloudflare:workers";
|
|
10
|
+
import { NonRetryableError } from "cloudflare:workflows";
|
|
11
|
+
import { classifiedSteps } from "@pithy-sh/core/src/workflow/faults";
|
|
12
|
+
import { workflowHostEntry } from "@pithy-sh/core/src/workflow/hostEntry";
|
|
13
|
+
//#region src/workflows/worker.ts
|
|
14
|
+
/** Classify one stored message and write the result. */
|
|
15
|
+
var SupportClassifyWorkflow = class extends WorkflowEntrypoint {
|
|
16
|
+
async run(event, step) {
|
|
17
|
+
const config = SupportConfig.parse(this.env.SUPPORT_CONFIG ? JSON.parse(this.env.SUPPORT_CONFIG) : {});
|
|
18
|
+
const deps = {
|
|
19
|
+
db: supportDatabase(this.env.DB),
|
|
20
|
+
ai: this.env.AI,
|
|
21
|
+
categories: resolveCategories(config.categories),
|
|
22
|
+
ai_config: config.ai,
|
|
23
|
+
archiveSpam: config.guard.archiveSpam,
|
|
24
|
+
newId: () => crypto.randomUUID(),
|
|
25
|
+
now: () => /* @__PURE__ */ new Date()
|
|
26
|
+
};
|
|
27
|
+
await classifiedSteps(step, supportWorkflowRetry, NonRetryableError).do(`classify-${event.payload.messageId}`, async () => {
|
|
28
|
+
await runClassification(deps, event.payload.messageId);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The module's default export, and therefore its format. See `hostEntry` for why a Workflow host needs
|
|
34
|
+
* one at all, and why this one refuses rather than being empty: nothing reaches this worker over HTTP —
|
|
35
|
+
* the app worker stores the message and starts an instance on the `SUPPORT_CLASSIFY` binding.
|
|
36
|
+
*/
|
|
37
|
+
var worker_default = workflowHostEntry(SUPPORT_CAPABILITY);
|
|
38
|
+
//#endregion
|
|
39
|
+
export { SupportClassifyWorkflow, worker_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/support",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,19 +8,24 @@
|
|
|
8
8
|
"directory": "packages/support"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
+
"dist",
|
|
11
12
|
"src",
|
|
12
13
|
"pithy.manifest.json",
|
|
13
|
-
"!src/**/*.test.*"
|
|
14
|
+
"!src/**/*.test.*",
|
|
15
|
+
"!dist/**/*.test.*"
|
|
14
16
|
],
|
|
15
17
|
"type": "module",
|
|
16
18
|
"engines": {
|
|
17
19
|
"node": ">=22"
|
|
18
20
|
},
|
|
19
21
|
"exports": {
|
|
20
|
-
"./src/*":
|
|
22
|
+
"./src/*": {
|
|
23
|
+
"types": "./dist/*.d.ts",
|
|
24
|
+
"default": "./dist/*.js"
|
|
25
|
+
}
|
|
21
26
|
},
|
|
22
27
|
"scripts": {
|
|
23
|
-
"build": "tsc -p tsconfig.json
|
|
28
|
+
"build": "tsdown && tsc -p tsconfig.build.json && bun ../../tooling/build/src/stampDeclarations.ts",
|
|
24
29
|
"typecheck": "tsc -p tsconfig.json",
|
|
25
30
|
"test": "vitest run",
|
|
26
31
|
"test:node": "vitest run --project=node",
|
|
@@ -31,19 +36,19 @@
|
|
|
31
36
|
"dependencies": {
|
|
32
37
|
"@cloudflare/workers-types": "^5.20260729.1",
|
|
33
38
|
"@hono/zod-validator": "^0.9.0",
|
|
34
|
-
"@pithy-sh/core": "^0.1.
|
|
35
|
-
"@pithy-sh/email": "^0.1.
|
|
36
|
-
"@pithy-sh/secrets": "^0.1.
|
|
37
|
-
"@pithy-sh/storage": "^0.1.
|
|
38
|
-
"hono": "^4.13.2",
|
|
39
|
-
"kysely": "^0.29.0",
|
|
39
|
+
"@pithy-sh/core": "^0.1.5",
|
|
40
|
+
"@pithy-sh/email": "^0.1.4",
|
|
41
|
+
"@pithy-sh/secrets": "^0.1.4",
|
|
42
|
+
"@pithy-sh/storage": "^0.1.4",
|
|
40
43
|
"postal-mime": "^3.0.0",
|
|
41
|
-
"tldts": "^7.4.10"
|
|
42
|
-
"zod": "^4.0.0"
|
|
44
|
+
"tldts": "^7.4.10"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
45
|
-
"@pithy-sh/auth": "^0.1.
|
|
46
|
-
"@pithy-sh/payments": "^0.1.
|
|
47
|
+
"@pithy-sh/auth": "^0.1.4",
|
|
48
|
+
"@pithy-sh/payments": "^0.1.5",
|
|
49
|
+
"hono": "^4.13.2",
|
|
50
|
+
"kysely": "^0.29.0",
|
|
51
|
+
"zod": "^4.4.0"
|
|
47
52
|
},
|
|
48
53
|
"peerDependenciesMeta": {
|
|
49
54
|
"@pithy-sh/auth": {
|
|
@@ -60,9 +65,13 @@
|
|
|
60
65
|
"@pithy-sh/tsconfig": "workspace:*",
|
|
61
66
|
"@types/node": "^22.15.0",
|
|
62
67
|
"@vitest/coverage-v8": "^4.1.0",
|
|
68
|
+
"hono": "^4.13.2",
|
|
69
|
+
"kysely": "^0.29.0",
|
|
63
70
|
"kysely-d1": "^0.4.0",
|
|
71
|
+
"tsdown": "^0.23.0",
|
|
64
72
|
"typescript": "^7.0.2",
|
|
65
73
|
"vitest": "^4.1.0",
|
|
66
|
-
"wrangler": "^4.115.0"
|
|
74
|
+
"wrangler": "^4.115.0",
|
|
75
|
+
"zod": "^4.4.0"
|
|
67
76
|
}
|
|
68
77
|
}
|
package/src/http/handlers.ts
CHANGED
|
@@ -359,7 +359,7 @@ export async function submitFeedbackRequest(
|
|
|
359
359
|
context: input.context,
|
|
360
360
|
// Decoded here, at the transport boundary, so `submitFeedback` takes bytes and stays testable
|
|
361
361
|
// without a request — the same split `ingest.ts` has from the `email()` handler. The decode is
|
|
362
|
-
// bounded before it allocates: `atob`
|
|
362
|
+
// bounded before it allocates: `atob` materializes the whole result, so a size check afterwards
|
|
363
363
|
// has already paid for the attack it was meant to refuse.
|
|
364
364
|
attachments: input.attachments.map((attachment) => ({
|
|
365
365
|
filename: attachment.filename,
|
|
@@ -19,7 +19,7 @@ const BASE64_EXPANSION = 4 / 3;
|
|
|
19
19
|
/**
|
|
20
20
|
* The largest base64 string worth decoding, derived from a decoded-byte bound.
|
|
21
21
|
*
|
|
22
|
-
* Checked **before** decoding, not after. `atob`
|
|
22
|
+
* Checked **before** decoding, not after. `atob` materializes the whole result, so a client that sends
|
|
23
23
|
* fifty megabytes of base64 has already been allocated fifty megabytes of Worker memory by the time a
|
|
24
24
|
* post-hoc size check could refuse it — and the request that does that is the one an attacker sends.
|
|
25
25
|
* A little slack for padding and any whitespace a client's encoder inserted.
|
package/src/submission/submit.ts
CHANGED
|
@@ -50,7 +50,7 @@ import { checkAccountRate, checkAttachment } from "./guard";
|
|
|
50
50
|
* The mail path's rule, applied to a different shape: **refuse on the cheapest true reason first.**
|
|
51
51
|
* The two shape bounds a client controls the cost of — how many files, and how large each encoded one
|
|
52
52
|
* is — are applied at the transport boundary in `http/handlers.ts` *before* anything is decoded,
|
|
53
|
-
* because `atob`
|
|
53
|
+
* because `atob` materializes its whole result and a size check after it has already been handed the
|
|
54
54
|
* allocation it meant to refuse. What is left arrives here as bytes:
|
|
55
55
|
*
|
|
56
56
|
* 0. **The declared category**, which needs no I/O at all — it is a comparison against a taxonomy this
|
package/src/version.generated.ts
CHANGED