@pithy-sh/support 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/audit/actions.d.ts +9 -0
- package/dist/audit/actions.d.ts.map +1 -1
- package/dist/audit/actions.js +9 -0
- package/dist/capability.d.ts +2 -1
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +30 -6
- package/dist/config/config.d.ts +63 -2
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +63 -5
- package/dist/data/assignment.d.ts +43 -0
- package/dist/data/assignment.d.ts.map +1 -0
- package/dist/data/assignment.js +38 -0
- package/dist/data/embedding.d.ts +21 -0
- package/dist/data/embedding.d.ts.map +1 -0
- package/dist/data/embedding.js +23 -0
- package/dist/data/tables.d.ts +28 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/tables.js +10 -2
- package/dist/data/thread.d.ts +9 -4
- package/dist/data/thread.d.ts.map +1 -1
- package/dist/data/thread.js +9 -4
- package/dist/http/guards.d.ts +32 -14
- package/dist/http/guards.d.ts.map +1 -1
- package/dist/http/guards.js +44 -16
- package/dist/http/handlers.d.ts +35 -2
- package/dist/http/handlers.d.ts.map +1 -1
- package/dist/http/handlers.js +81 -8
- package/dist/http/resolve.d.ts +8 -0
- package/dist/http/resolve.d.ts.map +1 -1
- package/dist/http/resolve.js +20 -1
- package/dist/http/responses.d.ts +355 -0
- package/dist/http/responses.d.ts.map +1 -1
- package/dist/http/responses.js +64 -2
- package/dist/http/routes.d.ts +25 -11
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +34 -5
- package/dist/http/schemas.d.ts +67 -3
- package/dist/http/schemas.d.ts.map +1 -1
- package/dist/http/schemas.js +44 -5
- package/dist/http/scopes.d.ts +10 -5
- package/dist/http/scopes.d.ts.map +1 -1
- package/dist/http/scopes.js +42 -6
- package/dist/http/views.d.ts +9 -1
- package/dist/http/views.d.ts.map +1 -1
- package/dist/http/views.js +40 -1
- package/dist/inbound/ingest.d.ts +1 -1
- package/dist/inbound/ingest.d.ts.map +1 -1
- package/dist/inbound/ingest.js +3 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/migrations/0002_assignment.d.ts +20 -0
- package/dist/migrations/0002_assignment.d.ts.map +1 -0
- package/dist/migrations/0002_assignment.js +45 -0
- package/dist/migrations/0003_embeddings.d.ts +15 -0
- package/dist/migrations/0003_embeddings.d.ts.map +1 -0
- package/dist/migrations/0003_embeddings.js +23 -0
- package/dist/migrations/set.d.ts +13 -0
- package/dist/migrations/set.d.ts.map +1 -0
- package/dist/migrations/set.js +21 -0
- package/dist/presence/room.d.ts +35 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +66 -0
- package/dist/provision/provisionSupport.d.ts +32 -0
- package/dist/provision/provisionSupport.d.ts.map +1 -1
- package/dist/provision/provisionSupport.js +10 -2
- package/dist/provision/resolveSupportConfig.d.ts.map +1 -1
- package/dist/provision/resolveSupportConfig.js +13 -2
- package/dist/purge/contribution.d.ts +49 -0
- package/dist/purge/contribution.d.ts.map +1 -0
- package/dist/purge/contribution.js +164 -0
- package/dist/secret/registry.d.ts +26 -2
- package/dist/secret/registry.d.ts.map +1 -1
- package/dist/secret/registry.js +15 -3
- package/dist/semantic/documents.d.ts +21 -0
- package/dist/semantic/documents.d.ts.map +1 -0
- package/dist/semantic/documents.js +128 -0
- package/dist/semantic/embed.d.ts +59 -0
- package/dist/semantic/embed.d.ts.map +1 -0
- package/dist/semantic/embed.js +59 -0
- package/dist/semantic/messageIndex.d.ts +46 -0
- package/dist/semantic/messageIndex.d.ts.map +1 -0
- package/dist/semantic/messageIndex.js +67 -0
- package/dist/semantic/query.d.ts +37 -0
- package/dist/semantic/query.d.ts.map +1 -0
- package/dist/semantic/query.js +46 -0
- package/dist/staff/staff.d.ts +22 -0
- package/dist/staff/staff.d.ts.map +1 -0
- package/dist/staff/staff.js +64 -0
- package/dist/store/assignment.d.ts +63 -0
- package/dist/store/assignment.d.ts.map +1 -0
- package/dist/store/assignment.js +86 -0
- package/dist/store/hybrid.d.ts +134 -0
- package/dist/store/hybrid.d.ts.map +1 -0
- package/dist/store/hybrid.js +185 -0
- package/dist/store/paging.d.ts +10 -0
- package/dist/store/paging.d.ts.map +1 -1
- package/dist/store/paging.js +11 -1
- package/dist/store/provisionIndex.d.ts +21 -0
- package/dist/store/provisionIndex.d.ts.map +1 -0
- package/dist/store/provisionIndex.js +36 -0
- package/dist/store/search.d.ts +18 -9
- package/dist/store/search.d.ts.map +1 -1
- package/dist/store/search.js +36 -34
- package/dist/store/searchIndex.d.ts +4 -4
- package/dist/store/searchIndex.js +4 -4
- package/dist/store/threads.d.ts +174 -8
- package/dist/store/threads.d.ts.map +1 -1
- package/dist/store/threads.js +33 -40
- package/dist/submission/submit.d.ts +1 -1
- package/dist/submission/submit.d.ts.map +1 -1
- package/dist/submission/submit.js +2 -1
- package/dist/test-utils/seed.d.ts +33 -0
- package/dist/test-utils/seed.d.ts.map +1 -0
- package/dist/test-utils/seed.js +102 -0
- package/dist/test-utils/semantic.d.ts +21 -0
- package/dist/test-utils/semantic.d.ts.map +1 -0
- package/dist/test-utils/semantic.js +107 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflows/specs.d.ts +23 -0
- package/dist/workflows/specs.d.ts.map +1 -1
- package/dist/workflows/specs.js +27 -7
- package/dist/workflows/worker.d.ts +15 -2
- package/dist/workflows/worker.d.ts.map +1 -1
- package/dist/workflows/worker.js +48 -4
- package/package.json +9 -7
- package/pithy.manifest.json +21 -3
- package/src/audit/actions.ts +9 -0
- package/src/capability.ts +43 -4
- package/src/config/config.ts +104 -5
- package/src/data/assignment.ts +55 -0
- package/src/data/embedding.ts +27 -0
- package/src/data/tables.ts +8 -0
- package/src/data/thread.ts +26 -4
- package/src/http/guards.ts +52 -15
- package/src/http/handlers.ts +125 -4
- package/src/http/resolve.ts +19 -0
- package/src/http/responses.ts +154 -0
- package/src/http/routes.ts +98 -12
- package/src/http/schemas.ts +117 -28
- package/src/http/scopes.ts +43 -6
- package/src/http/views.ts +44 -0
- package/src/inbound/ingest.ts +5 -2
- package/src/index.ts +5 -0
- package/src/migrations/0002_assignment.ts +65 -0
- package/src/migrations/0003_embeddings.ts +29 -0
- package/src/migrations/set.ts +21 -0
- package/src/presence/room.ts +94 -0
- package/src/provision/provisionSupport.ts +33 -1
- package/src/provision/resolveSupportConfig.ts +24 -13
- package/src/purge/contribution.ts +294 -0
- package/src/secret/registry.ts +17 -2
- package/src/semantic/documents.ts +178 -0
- package/src/semantic/embed.ts +99 -0
- package/src/semantic/messageIndex.ts +74 -0
- package/src/semantic/query.ts +66 -0
- package/src/staff/staff.ts +128 -0
- package/src/store/assignment.ts +179 -0
- package/src/store/hybrid.ts +314 -0
- package/src/store/paging.ts +14 -0
- package/src/store/provisionIndex.ts +46 -0
- package/src/store/search.ts +58 -37
- package/src/store/searchIndex.ts +4 -4
- package/src/store/threads.ts +91 -91
- package/src/submission/submit.ts +3 -2
- package/src/test-utils/seed.ts +141 -0
- package/src/test-utils/semantic.ts +110 -0
- package/src/version.generated.ts +1 -1
- package/src/workflows/specs.ts +34 -0
- package/src/workflows/worker.ts +84 -8
- package/src/workflows/wrangler.jsonc +8 -3
package/dist/data/thread.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ import { z } from "zod";
|
|
|
6
6
|
*
|
|
7
7
|
* **Everything on it is derived from immutable inbound mail.** The classification is recomputed, never
|
|
8
8
|
* repaired; the user link is a lookup, not a decision; the counters are facts about the messages
|
|
9
|
-
* below. There is no
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* below. There is no status, because a status field is a state machine and a state machine attracts
|
|
10
|
+
* SLAs, escalation, and reporting — which is a ticketing product and explicitly not this. The
|
|
11
|
+
* exceptions are deliberate and named: `archived` (one shared boolean meaning done), the assignee (one
|
|
12
|
+
* fact, who has this, with its history in `pithy_support_assignments` — #816), and the per-viewer
|
|
13
|
+
* flags, which live in their own table because nobody coordinates around them.
|
|
13
14
|
*
|
|
14
15
|
* The classification axes are **denormalized onto the thread** rather than joined from
|
|
15
16
|
* `pithy_support_classifications`. The whole dashboard interaction is "newest first, filtered by
|
|
@@ -52,6 +53,10 @@ export declare const SupportThread: z.ZodObject<{
|
|
|
52
53
|
archived: z.ZodCodec<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodBoolean, z.ZodString]>, z.ZodBoolean>;
|
|
53
54
|
archivedAt: z.ZodOptional<z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>>>;
|
|
54
55
|
archivedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
assigneeRef: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
assigneeName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
assignedAt: z.ZodOptional<z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>>>;
|
|
59
|
+
assignedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
55
60
|
messageCount: z.ZodNumber;
|
|
56
61
|
firstMessageAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
57
62
|
lastMessageAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/data/thread.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB
|
|
1
|
+
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/data/thread.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiHmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;;uBAEb,QAAQ;wBACP,SAAS;;;;CAOrB,CAAC"}
|
package/dist/data/thread.js
CHANGED
|
@@ -9,10 +9,11 @@ import { SQLiteBoolean, SQLiteDate } from "@pithy-sh/core/src/data/codecs";
|
|
|
9
9
|
*
|
|
10
10
|
* **Everything on it is derived from immutable inbound mail.** The classification is recomputed, never
|
|
11
11
|
* repaired; the user link is a lookup, not a decision; the counters are facts about the messages
|
|
12
|
-
* below. There is no
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* below. There is no status, because a status field is a state machine and a state machine attracts
|
|
13
|
+
* SLAs, escalation, and reporting — which is a ticketing product and explicitly not this. The
|
|
14
|
+
* exceptions are deliberate and named: `archived` (one shared boolean meaning done), the assignee (one
|
|
15
|
+
* fact, who has this, with its history in `pithy_support_assignments` — #816), and the per-viewer
|
|
16
|
+
* flags, which live in their own table because nobody coordinates around them.
|
|
16
17
|
*
|
|
17
18
|
* The classification axes are **denormalized onto the thread** rather than joined from
|
|
18
19
|
* `pithy_support_classifications`. The whole dashboard interaction is "newest first, filtered by
|
|
@@ -40,6 +41,10 @@ const SupportThread = z.object({
|
|
|
40
41
|
archived: SQLiteBoolean.describe("Done. One shared boolean, not per viewer, because if one person resolves a thread another should not still see it open. The mildest possible coordination state: no transitions to get wrong, nothing can be stuck, and unarchiving undoes a mistake instantly."),
|
|
41
42
|
archivedAt: SQLiteDate.nullish().describe("When the thread was last archived; null while it is open."),
|
|
42
43
|
archivedBy: z.string().nullish().describe("The control-plane subject that archived it. Recorded for the dashboard's convenience only — the answer to 'who marked this done' comes from the audit trail, which is why this is not an ownership column."),
|
|
44
|
+
assigneeRef: z.string().nullish().describe("**Who has this thread** — an opaque reference to the person or agent it is assigned to, or null when nobody has it. Opaque on purpose: a dashboard member, a user of the app and an agent are all assignable, and support never needs to know which. Not a status: there are no transitions to get wrong, and unassigning is one write. Every change is kept in `pithy_support_assignments`. Added by `0002_assignment`, so a row written before it reads null."),
|
|
45
|
+
assigneeName: z.string().nullish().describe("How the assignee is shown, as the assigner named them. Null exactly when `assigneeRef` is. Display text, never an identity: two refs may share a name, and the ref is what a filter matches."),
|
|
46
|
+
assignedAt: SQLiteDate.nullish().describe("When the current assignment was made; null while the thread has no assignee."),
|
|
47
|
+
assignedBy: z.string().nullish().describe("Who made the current assignment — the control-plane subject, or the signed-in user's id. Null while the thread has no assignee. The full answer, with every earlier change, is the history table and the audit trail."),
|
|
43
48
|
messageCount: z.number().int().describe("How many messages the thread holds, inbound and outbound together."),
|
|
44
49
|
firstMessageAt: SQLiteDate.describe("When the thread opened — the receive time of its first message."),
|
|
45
50
|
lastMessageAt: SQLiteDate.describe("When the thread last moved. The inbox sorts on this, and it is the `receivedAt` half of every composite index and of the pagination cursor."),
|
package/dist/http/guards.d.ts
CHANGED
|
@@ -2,35 +2,43 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
4
4
|
import type { MiddlewareHandler } from "hono";
|
|
5
|
+
import type { SupportStaffConfig } from "../config/config";
|
|
5
6
|
/**
|
|
6
|
-
* Support's identity gates: the copied `requireAuth()` its in-app
|
|
7
|
-
*
|
|
7
|
+
* Support's identity gates: the copied `requireAuth()` its in-app routes wear, the control-plane gate its
|
|
8
|
+
* management routes take from core's seam, and `requireStaff()`, which lets the adopter's own staff onto a
|
|
9
|
+
* slice of the management side through their session (#816).
|
|
8
10
|
*
|
|
9
11
|
* **The scope constants moved to `./scopes`** (#315). A management client reads them to render what a
|
|
10
12
|
* connection may do, and it reads them in a browser — so they cannot live in a module that imports
|
|
11
13
|
* Hono middleware and `PithyHonoEnv`. The gates stayed here; the names they demand are next door, and
|
|
12
14
|
* the routes import both.
|
|
13
15
|
*
|
|
14
|
-
* ##
|
|
16
|
+
* ## Three doors, and the split is the whole shape of this file
|
|
15
17
|
*
|
|
16
18
|
* **The management surface is `control-plane` and default-denied.** It reads and acts on every
|
|
17
|
-
* customer's private correspondence, so it answers to a credential the adopter issued
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* customer's private correspondence, so it answers to a credential the adopter issued. The seam's gate is
|
|
20
|
+
* imported from core rather than copied — core is a hard dependency of every capability, so importing its
|
|
21
|
+
* gate cannot leave a deployment without one, and with the seam uncomposed `requireControlPlane` raises
|
|
22
|
+
* `controlplane/not_connected` rather than passing.
|
|
21
23
|
*
|
|
22
24
|
* **The submission surface is `bearer`/`session` and is the adopter's own signed-in user.** It exists
|
|
23
25
|
* because a product with a logged-in user, a session, and a support console should not have to ask
|
|
24
26
|
* that user to open their mail client — and because the hardest problem on the mail path, proving a
|
|
25
27
|
* `From:` header, does not exist on a request whose session was already proved.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* **The staff door is a session, and it reaches management work.** `requireAuth()` then `requireStaff()`
|
|
30
|
+
* admit a signed-in user whose role, in the organization the request acts for, holds one of the powers
|
|
31
|
+
* `support({ staff })` names — and nobody at all when `staff` is absent. It opens two things and no more:
|
|
32
|
+
* assigning a thread, at its own `/staff/` path, and joining a thread's presence room. Everything else on
|
|
33
|
+
* the management side still answers to the control plane alone.
|
|
34
|
+
*
|
|
35
|
+
* **`requireAuth()` never appears on a control-plane route, and `requireControlPlane` never appears on a
|
|
36
|
+
* session route.** They are not two strengths of the same gate. A management client holds no session and
|
|
37
|
+
* owns no account row — core leaves `c.var.auth` null for one deliberately — so stacking them would deny
|
|
38
|
+
* every legitimate call on both, permanently, with no credential able to fix it. That is why staff
|
|
39
|
+
* assignment is a second route beside the control-plane one rather than a second gate on it. What each
|
|
40
|
+
* route may *see* follows from which gate it wears: an operator reads a thread with its classification and
|
|
41
|
+
* its sender's purchases, and a submitter reads their own words back and nothing else.
|
|
34
42
|
*/
|
|
35
43
|
/**
|
|
36
44
|
* Require an authenticated caller — core's `AuthContext` seam, and the gate on every in-app route.
|
|
@@ -43,4 +51,14 @@ import type { MiddlewareHandler } from "hono";
|
|
|
43
51
|
* gate is somebody's support history.
|
|
44
52
|
*/
|
|
45
53
|
export declare function requireAuth(): MiddlewareHandler<PithyHonoEnv>;
|
|
54
|
+
/**
|
|
55
|
+
* Require one of support's staff powers in the organization this request acts for (#816) — the gate on
|
|
56
|
+
* the staff session routes, after `requireAuth()`.
|
|
57
|
+
*
|
|
58
|
+
* With no `staff` configured, or no organization composed, or no organization in force, every caller is
|
|
59
|
+
* refused: the inbox is every customer's correspondence, and the absence of a policy is never an open one.
|
|
60
|
+
* The refusal names no power and no organization, because which power reaches the inbox is the adopter's
|
|
61
|
+
* internal arrangement and a caller who does not hold it has no use for the name.
|
|
62
|
+
*/
|
|
63
|
+
export declare function requireStaff(staff: SupportStaffConfig | undefined, power: keyof SupportStaffConfig): MiddlewareHandler<PithyHonoEnv>;
|
|
46
64
|
//# sourceMappingURL=guards.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/http/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/http/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAU7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,kBAAkB,GAAG,SAAS,EACrC,KAAK,EAAE,MAAM,kBAAkB,GAC9B,iBAAiB,CAAC,YAAY,CAAC,CAcjC"}
|
package/dist/http/guards.js
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import {
|
|
3
|
+
import { staffMay } from "../staff/staff.js";
|
|
4
|
+
import { ForbiddenError, UnauthorizedError } from "@pithy-sh/core/src/error/pithyError";
|
|
4
5
|
//#region src/http/guards.ts
|
|
5
6
|
/**
|
|
6
|
-
* Support's identity gates: the copied `requireAuth()` its in-app
|
|
7
|
-
*
|
|
7
|
+
* Support's identity gates: the copied `requireAuth()` its in-app routes wear, the control-plane gate its
|
|
8
|
+
* management routes take from core's seam, and `requireStaff()`, which lets the adopter's own staff onto a
|
|
9
|
+
* slice of the management side through their session (#816).
|
|
8
10
|
*
|
|
9
11
|
* **The scope constants moved to `./scopes`** (#315). A management client reads them to render what a
|
|
10
12
|
* connection may do, and it reads them in a browser — so they cannot live in a module that imports
|
|
11
13
|
* Hono middleware and `PithyHonoEnv`. The gates stayed here; the names they demand are next door, and
|
|
12
14
|
* the routes import both.
|
|
13
15
|
*
|
|
14
|
-
* ##
|
|
16
|
+
* ## Three doors, and the split is the whole shape of this file
|
|
15
17
|
*
|
|
16
18
|
* **The management surface is `control-plane` and default-denied.** It reads and acts on every
|
|
17
|
-
* customer's private correspondence, so it answers to a credential the adopter issued
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* customer's private correspondence, so it answers to a credential the adopter issued. The seam's gate is
|
|
20
|
+
* imported from core rather than copied — core is a hard dependency of every capability, so importing its
|
|
21
|
+
* gate cannot leave a deployment without one, and with the seam uncomposed `requireControlPlane` raises
|
|
22
|
+
* `controlplane/not_connected` rather than passing.
|
|
21
23
|
*
|
|
22
24
|
* **The submission surface is `bearer`/`session` and is the adopter's own signed-in user.** It exists
|
|
23
25
|
* because a product with a logged-in user, a session, and a support console should not have to ask
|
|
24
26
|
* that user to open their mail client — and because the hardest problem on the mail path, proving a
|
|
25
27
|
* `From:` header, does not exist on a request whose session was already proved.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* **The staff door is a session, and it reaches management work.** `requireAuth()` then `requireStaff()`
|
|
30
|
+
* admit a signed-in user whose role, in the organization the request acts for, holds one of the powers
|
|
31
|
+
* `support({ staff })` names — and nobody at all when `staff` is absent. It opens two things and no more:
|
|
32
|
+
* assigning a thread, at its own `/staff/` path, and joining a thread's presence room. Everything else on
|
|
33
|
+
* the management side still answers to the control plane alone.
|
|
34
|
+
*
|
|
35
|
+
* **`requireAuth()` never appears on a control-plane route, and `requireControlPlane` never appears on a
|
|
36
|
+
* session route.** They are not two strengths of the same gate. A management client holds no session and
|
|
37
|
+
* owns no account row — core leaves `c.var.auth` null for one deliberately — so stacking them would deny
|
|
38
|
+
* every legitimate call on both, permanently, with no credential able to fix it. That is why staff
|
|
39
|
+
* assignment is a second route beside the control-plane one rather than a second gate on it. What each
|
|
40
|
+
* route may *see* follows from which gate it wears: an operator reads a thread with its classification and
|
|
41
|
+
* its sender's purchases, and a submitter reads their own words back and nothing else.
|
|
34
42
|
*/
|
|
35
43
|
/**
|
|
36
44
|
* Require an authenticated caller — core's `AuthContext` seam, and the gate on every in-app route.
|
|
@@ -51,5 +59,25 @@ function requireAuth() {
|
|
|
51
59
|
await next();
|
|
52
60
|
};
|
|
53
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Require one of support's staff powers in the organization this request acts for (#816) — the gate on
|
|
64
|
+
* the staff session routes, after `requireAuth()`.
|
|
65
|
+
*
|
|
66
|
+
* With no `staff` configured, or no organization composed, or no organization in force, every caller is
|
|
67
|
+
* refused: the inbox is every customer's correspondence, and the absence of a policy is never an open one.
|
|
68
|
+
* The refusal names no power and no organization, because which power reaches the inbox is the adopter's
|
|
69
|
+
* internal arrangement and a caller who does not hold it has no use for the name.
|
|
70
|
+
*/
|
|
71
|
+
function requireStaff(staff, power) {
|
|
72
|
+
return async (c, next) => {
|
|
73
|
+
const acting = await c.var.actingOrganization();
|
|
74
|
+
if (!staffMay(acting, staff, power)) throw new ForbiddenError({
|
|
75
|
+
message: "You do not work this support inbox.",
|
|
76
|
+
action: staff ? `Give the user an unassignable role holding \`${staff[power]}\`, through the control plane.` : "Name staff powers in support({ staff }) to let signed-in users work the inbox.",
|
|
77
|
+
detail: `user ${c.var.auth?.userId ?? "unknown"} holds no ${power} for support in ${acting?.organizationId ?? "no organization"}`
|
|
78
|
+
});
|
|
79
|
+
await next();
|
|
80
|
+
};
|
|
81
|
+
}
|
|
54
82
|
//#endregion
|
|
55
|
-
export { requireAuth };
|
|
83
|
+
export { requireAuth, requireStaff };
|
package/dist/http/handlers.d.ts
CHANGED
|
@@ -4,14 +4,18 @@ import type { D1Database, R2Bucket } from "@cloudflare/workers-types";
|
|
|
4
4
|
import type { AuditEmit } from "@pithy-sh/core/src/audit/recorder";
|
|
5
5
|
import type { Logger } from "@pithy-sh/core/src/logger/logger";
|
|
6
6
|
import type { ObjectStore } from "@pithy-sh/storage/src/object/store";
|
|
7
|
+
import type { VectorIndexConfig } from "@pithy-sh/vector/src/config/config";
|
|
8
|
+
import type { VectorAi } from "@pithy-sh/vector/src/embed/embed";
|
|
9
|
+
import type { VectorStore } from "@pithy-sh/vector/src/index/index";
|
|
7
10
|
import type { SupportConfig } from "../config/config";
|
|
11
|
+
import type { SupportAssignmentActorType } from "../data/assignment";
|
|
8
12
|
import type { SupportCategories } from "../data/categories";
|
|
9
13
|
import type { SupportDatabase } from "../data/tables";
|
|
10
14
|
import { type SenderPeers } from "../link/sender";
|
|
11
15
|
import { sendReply } from "../reply/send";
|
|
12
16
|
import type { SupportReplySnippets } from "../reply/snippets";
|
|
13
|
-
import type { SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportReplySentResponse, SupportReplyView, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse, SupportThreadView } from "./responses";
|
|
14
|
-
import type { ArchiveThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SubmitFeedbackInput } from "./schemas";
|
|
17
|
+
import type { SupportAssignResponse, SupportFlagsResponse, SupportMyThreadResponse, SupportMyThreadsResponse, SupportReclassifiedResponse, SupportReplySentResponse, SupportReplyView, SupportSearchResponse, SupportSubmissionResponse, SupportThreadResponse, SupportThreadsResponse, SupportThreadView } from "./responses";
|
|
18
|
+
import type { ArchiveThreadInput, AssignThreadInput, FlagsInput, ListThreadsQuery, MyThreadsQuery, RepliesQuery, ReplyInput, SearchThreadsQuery, SubmitFeedbackInput } from "./schemas";
|
|
15
19
|
/**
|
|
16
20
|
* The support handlers — every one of them behind the `control-plane` gate, and every one of them
|
|
17
21
|
* taking already-validated values.
|
|
@@ -33,6 +37,15 @@ export interface HandlerDeps {
|
|
|
33
37
|
snippets: SupportReplySnippets;
|
|
34
38
|
/** Whether the FTS5 index is composed. */
|
|
35
39
|
fts: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The semantic message index and the bindings it runs on. `index` is absent when `search.semantic` is
|
|
42
|
+
* off; `ai` and `store` are absent when this Worker was not given them — until `pithy support provision`.
|
|
43
|
+
*/
|
|
44
|
+
semantic: {
|
|
45
|
+
index?: VectorIndexConfig;
|
|
46
|
+
ai?: VectorAi;
|
|
47
|
+
store?: VectorStore;
|
|
48
|
+
};
|
|
36
49
|
/** The attachment bucket, when one is bound. */
|
|
37
50
|
bucket?: R2Bucket;
|
|
38
51
|
/** The object store attachment URLs are signed through, when credentials are available. */
|
|
@@ -54,10 +67,30 @@ export interface HandlerDeps {
|
|
|
54
67
|
}
|
|
55
68
|
/** List the inbox. */
|
|
56
69
|
export declare function listInbox(deps: HandlerDeps, query: ListThreadsQuery, viewer: string): Promise<SupportThreadsResponse>;
|
|
70
|
+
/**
|
|
71
|
+
* Search the inbox: keyword and semantic, ranked as one. The semantic seams are whatever this Worker
|
|
72
|
+
* was given — absent bindings skip that backend and say so, rather than failing the search.
|
|
73
|
+
*/
|
|
74
|
+
export declare function searchInbox(deps: HandlerDeps, query: SearchThreadsQuery, viewer: string): Promise<SupportSearchResponse>;
|
|
57
75
|
/** Read one conversation in full. */
|
|
58
76
|
export declare function readConversation(deps: HandlerDeps, threadId: string): Promise<SupportThreadResponse>;
|
|
59
77
|
/** Mark a conversation done, or reopen it. */
|
|
60
78
|
export declare function archiveConversation(deps: HandlerDeps, threadId: string, input: ArchiveThreadInput, viewer: string): Promise<SupportThreadView>;
|
|
79
|
+
/** Who is assigning: how they were verified, and their id under it. */
|
|
80
|
+
export interface Assigner {
|
|
81
|
+
/** `control-plane` for a management client, `user` for a signed-in user holding support's staff power. */
|
|
82
|
+
type: SupportAssignmentActorType;
|
|
83
|
+
/** The control-plane subject, or the user's id. */
|
|
84
|
+
id: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assign a conversation to somebody, or to nobody — the one handler behind both doors.
|
|
88
|
+
*
|
|
89
|
+
* The audit event is emitted **only when something changed**. The current assignee again, or unassigning
|
|
90
|
+
* a thread nobody has, wrote nothing to the thread or its history, so an event would record a handover
|
|
91
|
+
* that did not happen.
|
|
92
|
+
*/
|
|
93
|
+
export declare function assignConversation(deps: HandlerDeps, threadId: string, input: AssignThreadInput, assigner: Assigner): Promise<SupportAssignResponse>;
|
|
61
94
|
/** Answer the customer. */
|
|
62
95
|
export declare function replyToConversation(deps: HandlerDeps, threadId: string, input: ReplyInput, viewer: string): Promise<SupportReplySentResponse>;
|
|
63
96
|
/** Re-run the classifier over a conversation's latest inbound message. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAGnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"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"}
|
package/dist/http/handlers.js
CHANGED
|
@@ -6,11 +6,13 @@ import { attachmentUrl } from "../attachment/store.js";
|
|
|
6
6
|
import { SupportAuditActions } from "../audit/actions.js";
|
|
7
7
|
import { resolveSenderContext, resolveSubmitterAccount, resolveSubmitterContext } from "../link/sender.js";
|
|
8
8
|
import { sendReply } from "../reply/send.js";
|
|
9
|
+
import { assignThread, listAssignments } from "../store/assignment.js";
|
|
9
10
|
import { listOwnThreads, listThreads, readOwnThread, readThread, setArchived, setFlags } from "../store/threads.js";
|
|
11
|
+
import { searchThreads } from "../store/hybrid.js";
|
|
10
12
|
import { decodeBase64 } from "../submission/encoding.js";
|
|
11
13
|
import { submitFeedback } from "../submission/submit.js";
|
|
12
14
|
import { latestInboundMessageId } from "../workflows/classify.js";
|
|
13
|
-
import { listedThreadView, messageView, myMessageView, myThreadView, senderView, threadView } from "./views.js";
|
|
15
|
+
import { assignmentView, listedThreadView, messageView, myMessageView, myThreadView, searchView, senderView, threadView } from "./views.js";
|
|
14
16
|
import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
|
|
15
17
|
import { pithyCommandOf } from "@pithy-sh/core/src/cli/command";
|
|
16
18
|
//#region src/http/handlers.ts
|
|
@@ -24,14 +26,12 @@ async function listInbox(deps, query, viewer) {
|
|
|
24
26
|
sentiment: query.sentiment,
|
|
25
27
|
channel: query.channel,
|
|
26
28
|
inbox: query.inbox,
|
|
27
|
-
|
|
29
|
+
assignee: query.assignee,
|
|
28
30
|
cursor: query.cursor,
|
|
29
31
|
limit: query.limit
|
|
30
32
|
}, {
|
|
31
|
-
fts: deps.fts,
|
|
32
33
|
viewer,
|
|
33
|
-
now: deps.now()
|
|
34
|
-
log: deps.log
|
|
34
|
+
now: deps.now()
|
|
35
35
|
});
|
|
36
36
|
return {
|
|
37
37
|
threads: page.threads.map(listedThreadView),
|
|
@@ -39,6 +39,34 @@ async function listInbox(deps, query, viewer) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
+
* Search the inbox: keyword and semantic, ranked as one. The semantic seams are whatever this Worker
|
|
43
|
+
* was given — absent bindings skip that backend and say so, rather than failing the search.
|
|
44
|
+
*/
|
|
45
|
+
async function searchInbox(deps, query, viewer) {
|
|
46
|
+
const outcome = await searchThreads({
|
|
47
|
+
db: deps.db,
|
|
48
|
+
fts: deps.fts,
|
|
49
|
+
...deps.semantic.index ? { index: deps.semantic.index } : {},
|
|
50
|
+
...deps.semantic.ai ? { ai: deps.semantic.ai } : {},
|
|
51
|
+
...deps.semantic.store ? { store: deps.semantic.store } : {},
|
|
52
|
+
viewer,
|
|
53
|
+
log: deps.log
|
|
54
|
+
}, {
|
|
55
|
+
q: query.q,
|
|
56
|
+
...query.mode !== void 0 ? { mode: query.mode } : {},
|
|
57
|
+
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
58
|
+
archived: query.archived === "true",
|
|
59
|
+
category: query.category,
|
|
60
|
+
declaredCategory: query.declaredCategory,
|
|
61
|
+
priority: query.priority,
|
|
62
|
+
sentiment: query.sentiment,
|
|
63
|
+
channel: query.channel,
|
|
64
|
+
inbox: query.inbox,
|
|
65
|
+
assignee: query.assignee
|
|
66
|
+
});
|
|
67
|
+
return searchView(outcome);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
42
70
|
* Present an attachment.
|
|
43
71
|
*
|
|
44
72
|
* The `storageKey` is **never** in the response. It is server-derived and opaque precisely so a
|
|
@@ -70,13 +98,18 @@ async function presentAttachments(deps, attachments) {
|
|
|
70
98
|
/** Read one conversation in full. */
|
|
71
99
|
async function readConversation(deps, threadId) {
|
|
72
100
|
const detail = await readThread(deps.db, threadId);
|
|
73
|
-
const [attachments, sender] = await Promise.all([
|
|
101
|
+
const [attachments, sender, assignments] = await Promise.all([
|
|
102
|
+
presentAttachments(deps, detail.attachments),
|
|
103
|
+
detail.thread.accountLinkSource === "session" && detail.thread.userId ? resolveSubmitterContext(deps.d1, detail.thread.userId, deps.now(), deps.peers) : resolveSenderContext(deps.d1, detail.thread.fromAddress, deps.now(), { authenticated: detail.thread.senderAuthenticated }, deps.peers),
|
|
104
|
+
listAssignments(deps.db, threadId)
|
|
105
|
+
]);
|
|
74
106
|
return {
|
|
75
107
|
thread: threadView(detail.thread),
|
|
76
108
|
messages: detail.messages.map(messageView),
|
|
77
109
|
attachments,
|
|
78
110
|
sender: senderView(sender),
|
|
79
|
-
replies: repliesForCategory(deps.snippets, detail.thread.category)
|
|
111
|
+
replies: repliesForCategory(deps.snippets, detail.thread.category),
|
|
112
|
+
assignments: assignments.map(assignmentView)
|
|
80
113
|
};
|
|
81
114
|
}
|
|
82
115
|
/** Mark a conversation done, or reopen it. */
|
|
@@ -93,6 +126,41 @@ async function archiveConversation(deps, threadId, input, viewer) {
|
|
|
93
126
|
});
|
|
94
127
|
return threadView(thread);
|
|
95
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Assign a conversation to somebody, or to nobody — the one handler behind both doors.
|
|
131
|
+
*
|
|
132
|
+
* The audit event is emitted **only when something changed**. The current assignee again, or unassigning
|
|
133
|
+
* a thread nobody has, wrote nothing to the thread or its history, so an event would record a handover
|
|
134
|
+
* that did not happen.
|
|
135
|
+
*/
|
|
136
|
+
async function assignConversation(deps, threadId, input, assigner) {
|
|
137
|
+
const result = await assignThread(deps.d1, {
|
|
138
|
+
threadId,
|
|
139
|
+
assignee: input.assignee,
|
|
140
|
+
actorType: assigner.type,
|
|
141
|
+
assignedBy: assigner.id,
|
|
142
|
+
note: input.note,
|
|
143
|
+
newId: deps.newId,
|
|
144
|
+
now: deps.now()
|
|
145
|
+
});
|
|
146
|
+
if (result.changed) await deps.emit({
|
|
147
|
+
action: input.assignee ? SupportAuditActions.threadAssigned : SupportAuditActions.threadUnassigned,
|
|
148
|
+
outcome: "success",
|
|
149
|
+
actorType: assigner.type,
|
|
150
|
+
actorId: assigner.id,
|
|
151
|
+
resourceType: "support_thread",
|
|
152
|
+
resourceId: threadId,
|
|
153
|
+
metadata: {
|
|
154
|
+
previousAssignee: result.previous,
|
|
155
|
+
assignee: input.assignee,
|
|
156
|
+
note: input.note ?? null
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
thread: threadView(result.thread),
|
|
161
|
+
changed: result.changed
|
|
162
|
+
};
|
|
163
|
+
}
|
|
96
164
|
/** Answer the customer. */
|
|
97
165
|
async function replyToConversation(deps, threadId, input, viewer) {
|
|
98
166
|
return sendReply({
|
|
@@ -112,6 +180,11 @@ async function replyToConversation(deps, threadId, input, viewer) {
|
|
|
112
180
|
}
|
|
113
181
|
/** Re-run the classifier over a conversation's latest inbound message. */
|
|
114
182
|
async function reclassifyConversation(deps, threadId, viewer) {
|
|
183
|
+
if (!deps.config.ai.enabled) throw new SupportClassificationError({
|
|
184
|
+
message: "Classification is off for this project.",
|
|
185
|
+
action: `Turn on \`ai.enabled\` in the support config, run \`${pithyCommandOf("support provision", {}, { runtime: true })}\`, then retry.`,
|
|
186
|
+
detail: `ai.enabled is false, so no instance was started for thread ${threadId}`
|
|
187
|
+
});
|
|
115
188
|
const messageId = await latestInboundMessageId(deps.db, threadId);
|
|
116
189
|
if (!messageId) throw new SupportNotFoundError({
|
|
117
190
|
message: "That conversation has nothing to classify.",
|
|
@@ -246,4 +319,4 @@ async function readMyThread(deps, threadId, userId) {
|
|
|
246
319
|
};
|
|
247
320
|
}
|
|
248
321
|
//#endregion
|
|
249
|
-
export { archiveConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, submitFeedbackRequest, updateFlags };
|
|
322
|
+
export { archiveConversation, assignConversation, listInbox, listMyThreads, listReplies, readConversation, readMyThread, reclassifyConversation, replyToConversation, searchInbox, submitFeedbackRequest, updateFlags };
|
package/dist/http/resolve.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SecretsStoreEnv } from "@pithy-sh/secrets/src/env/bindings";
|
|
|
7
7
|
import { objectStore } from "@pithy-sh/storage/src/object/store";
|
|
8
8
|
import type { Context } from "hono";
|
|
9
9
|
import type { SupportWiring } from "../capability";
|
|
10
|
+
import type { SupportConfig } from "../config/config";
|
|
10
11
|
import type { HandlerDeps } from "./handlers";
|
|
11
12
|
/**
|
|
12
13
|
* Turning a Worker env into the dependencies the handlers take.
|
|
@@ -41,6 +42,13 @@ export declare function makeClassifyDispatcher(env: Record<string, unknown>, log
|
|
|
41
42
|
* inside the seam, so this costs nothing until something is actually signed.
|
|
42
43
|
*/
|
|
43
44
|
export declare function resolveStore(env: SupportEnv): ReturnType<typeof objectStore> | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The semantic index and the two bindings it runs on, as this Worker has them.
|
|
47
|
+
*
|
|
48
|
+
* Absent rather than refused: a project that turned `search.semantic` on and has not provisioned yet still
|
|
49
|
+
* serves every route, and the search route names the backend it skipped.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveSemantic(env: Record<string, unknown>, config: SupportConfig): HandlerDeps["semantic"];
|
|
44
52
|
/** Build the per-request handler dependencies. */
|
|
45
53
|
export declare function makeResolveDeps(wiring: SupportWiring): (c: Context<PithyHonoEnv>) => Promise<HandlerDeps>;
|
|
46
54
|
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/http/resolve.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/http/resolve.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAGjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAKtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;GAMG;AAEH,kCAAkC;AAClC,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,qDAAqD;IACrD,EAAE,EAAE,UAAU,CAAC;IACf,mGAAmG;IACnG,cAAc,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAED,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAUlE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,MAAM,GACV,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAUzC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,SAAS,CAGxF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,aAAa,GAAG,WAAW,CAAC,UAAU,CAAC,CAM5G;AAED,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CA+BzG"}
|
package/dist/http/resolve.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { supportDatabase } from "../data/tables.js";
|
|
4
4
|
import { SUPPORT_R2_SECRET } from "../secret/registry.js";
|
|
5
|
+
import { SUPPORT_VECTORIZE_BINDING, supportMessageIndex } from "../semantic/messageIndex.js";
|
|
5
6
|
import { supportWorkflowRegistry, supportWorkflows } from "../workflows/specs.js";
|
|
6
7
|
import { InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
7
8
|
import { objectStore } from "@pithy-sh/storage/src/object/store";
|
|
@@ -46,6 +47,23 @@ function resolveStore(env) {
|
|
|
46
47
|
secretName: SUPPORT_R2_SECRET
|
|
47
48
|
});
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The semantic index and the two bindings it runs on, as this Worker has them.
|
|
52
|
+
*
|
|
53
|
+
* Absent rather than refused: a project that turned `search.semantic` on and has not provisioned yet still
|
|
54
|
+
* serves every route, and the search route names the backend it skipped.
|
|
55
|
+
*/
|
|
56
|
+
function resolveSemantic(env, config) {
|
|
57
|
+
const index = supportMessageIndex(config.search);
|
|
58
|
+
if (!index) return {};
|
|
59
|
+
const ai = env["AI"];
|
|
60
|
+
const store = env[SUPPORT_VECTORIZE_BINDING];
|
|
61
|
+
return {
|
|
62
|
+
index,
|
|
63
|
+
...ai ? { ai } : {},
|
|
64
|
+
...store ? { store } : {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
49
67
|
/** Build the per-request handler dependencies. */
|
|
50
68
|
function makeResolveDeps(wiring) {
|
|
51
69
|
return async (c) => {
|
|
@@ -59,6 +77,7 @@ function makeResolveDeps(wiring) {
|
|
|
59
77
|
categories: wiring.categories,
|
|
60
78
|
snippets: wiring.snippets,
|
|
61
79
|
fts: wiring.config.search.fts,
|
|
80
|
+
semantic: resolveSemantic(env, wiring.config),
|
|
62
81
|
bucket: env.SUPPORT_BUCKET,
|
|
63
82
|
store: resolveStore(env),
|
|
64
83
|
enqueue: wiring.enqueueEmail ? (input) => wiring.enqueueEmail?.(env, input) : void 0,
|
|
@@ -72,4 +91,4 @@ function makeResolveDeps(wiring) {
|
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
//#endregion
|
|
75
|
-
export { makeClassifyDispatcher, makeResolveDeps, resolveDb, resolveStore };
|
|
94
|
+
export { makeClassifyDispatcher, makeResolveDeps, resolveDb, resolveSemantic, resolveStore };
|