@pithy-sh/support 0.5.1 → 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.
Files changed (173) hide show
  1. package/README.md +8 -0
  2. package/dist/audit/actions.d.ts +9 -0
  3. package/dist/audit/actions.d.ts.map +1 -1
  4. package/dist/audit/actions.js +9 -0
  5. package/dist/capability.d.ts +2 -1
  6. package/dist/capability.d.ts.map +1 -1
  7. package/dist/capability.js +30 -6
  8. package/dist/config/config.d.ts +63 -2
  9. package/dist/config/config.d.ts.map +1 -1
  10. package/dist/config/config.js +63 -5
  11. package/dist/data/assignment.d.ts +43 -0
  12. package/dist/data/assignment.d.ts.map +1 -0
  13. package/dist/data/assignment.js +38 -0
  14. package/dist/data/embedding.d.ts +21 -0
  15. package/dist/data/embedding.d.ts.map +1 -0
  16. package/dist/data/embedding.js +23 -0
  17. package/dist/data/tables.d.ts +28 -0
  18. package/dist/data/tables.d.ts.map +1 -1
  19. package/dist/data/tables.js +10 -2
  20. package/dist/data/thread.d.ts +9 -4
  21. package/dist/data/thread.d.ts.map +1 -1
  22. package/dist/data/thread.js +9 -4
  23. package/dist/http/guards.d.ts +32 -14
  24. package/dist/http/guards.d.ts.map +1 -1
  25. package/dist/http/guards.js +44 -16
  26. package/dist/http/handlers.d.ts +35 -2
  27. package/dist/http/handlers.d.ts.map +1 -1
  28. package/dist/http/handlers.js +81 -8
  29. package/dist/http/resolve.d.ts +8 -0
  30. package/dist/http/resolve.d.ts.map +1 -1
  31. package/dist/http/resolve.js +20 -1
  32. package/dist/http/responses.d.ts +355 -0
  33. package/dist/http/responses.d.ts.map +1 -1
  34. package/dist/http/responses.js +64 -2
  35. package/dist/http/routes.d.ts +25 -11
  36. package/dist/http/routes.d.ts.map +1 -1
  37. package/dist/http/routes.js +34 -5
  38. package/dist/http/schemas.d.ts +67 -3
  39. package/dist/http/schemas.d.ts.map +1 -1
  40. package/dist/http/schemas.js +44 -5
  41. package/dist/http/scopes.d.ts +10 -5
  42. package/dist/http/scopes.d.ts.map +1 -1
  43. package/dist/http/scopes.js +42 -6
  44. package/dist/http/views.d.ts +9 -1
  45. package/dist/http/views.d.ts.map +1 -1
  46. package/dist/http/views.js +40 -1
  47. package/dist/inbound/ingest.d.ts +1 -1
  48. package/dist/inbound/ingest.d.ts.map +1 -1
  49. package/dist/inbound/ingest.js +3 -1
  50. package/dist/index.d.ts +5 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +6 -4
  53. package/dist/migrations/0002_assignment.d.ts +20 -0
  54. package/dist/migrations/0002_assignment.d.ts.map +1 -0
  55. package/dist/migrations/0002_assignment.js +45 -0
  56. package/dist/migrations/0003_embeddings.d.ts +15 -0
  57. package/dist/migrations/0003_embeddings.d.ts.map +1 -0
  58. package/dist/migrations/0003_embeddings.js +23 -0
  59. package/dist/migrations/set.d.ts +13 -0
  60. package/dist/migrations/set.d.ts.map +1 -0
  61. package/dist/migrations/set.js +21 -0
  62. package/dist/presence/room.d.ts +35 -0
  63. package/dist/presence/room.d.ts.map +1 -0
  64. package/dist/presence/room.js +66 -0
  65. package/dist/provision/provisionSupport.d.ts +32 -0
  66. package/dist/provision/provisionSupport.d.ts.map +1 -1
  67. package/dist/provision/provisionSupport.js +10 -2
  68. package/dist/provision/resolveSupportConfig.d.ts.map +1 -1
  69. package/dist/provision/resolveSupportConfig.js +13 -2
  70. package/dist/purge/contribution.d.ts +49 -0
  71. package/dist/purge/contribution.d.ts.map +1 -0
  72. package/dist/purge/contribution.js +164 -0
  73. package/dist/secret/registry.d.ts +26 -2
  74. package/dist/secret/registry.d.ts.map +1 -1
  75. package/dist/secret/registry.js +15 -3
  76. package/dist/semantic/documents.d.ts +21 -0
  77. package/dist/semantic/documents.d.ts.map +1 -0
  78. package/dist/semantic/documents.js +128 -0
  79. package/dist/semantic/embed.d.ts +59 -0
  80. package/dist/semantic/embed.d.ts.map +1 -0
  81. package/dist/semantic/embed.js +59 -0
  82. package/dist/semantic/messageIndex.d.ts +46 -0
  83. package/dist/semantic/messageIndex.d.ts.map +1 -0
  84. package/dist/semantic/messageIndex.js +67 -0
  85. package/dist/semantic/query.d.ts +37 -0
  86. package/dist/semantic/query.d.ts.map +1 -0
  87. package/dist/semantic/query.js +46 -0
  88. package/dist/staff/staff.d.ts +22 -0
  89. package/dist/staff/staff.d.ts.map +1 -0
  90. package/dist/staff/staff.js +64 -0
  91. package/dist/store/assignment.d.ts +63 -0
  92. package/dist/store/assignment.d.ts.map +1 -0
  93. package/dist/store/assignment.js +86 -0
  94. package/dist/store/hybrid.d.ts +134 -0
  95. package/dist/store/hybrid.d.ts.map +1 -0
  96. package/dist/store/hybrid.js +185 -0
  97. package/dist/store/paging.d.ts +10 -0
  98. package/dist/store/paging.d.ts.map +1 -1
  99. package/dist/store/paging.js +11 -1
  100. package/dist/store/provisionIndex.d.ts +21 -0
  101. package/dist/store/provisionIndex.d.ts.map +1 -0
  102. package/dist/store/provisionIndex.js +36 -0
  103. package/dist/store/search.d.ts +18 -9
  104. package/dist/store/search.d.ts.map +1 -1
  105. package/dist/store/search.js +36 -34
  106. package/dist/store/searchIndex.d.ts +4 -4
  107. package/dist/store/searchIndex.js +4 -4
  108. package/dist/store/threads.d.ts +174 -8
  109. package/dist/store/threads.d.ts.map +1 -1
  110. package/dist/store/threads.js +33 -40
  111. package/dist/submission/submit.d.ts +1 -1
  112. package/dist/submission/submit.d.ts.map +1 -1
  113. package/dist/submission/submit.js +2 -1
  114. package/dist/test-utils/seed.d.ts +33 -0
  115. package/dist/test-utils/seed.d.ts.map +1 -0
  116. package/dist/test-utils/seed.js +102 -0
  117. package/dist/test-utils/semantic.d.ts +21 -0
  118. package/dist/test-utils/semantic.d.ts.map +1 -0
  119. package/dist/test-utils/semantic.js +107 -0
  120. package/dist/version.generated.d.ts +1 -1
  121. package/dist/version.generated.js +1 -1
  122. package/dist/workflows/specs.d.ts +23 -0
  123. package/dist/workflows/specs.d.ts.map +1 -1
  124. package/dist/workflows/specs.js +27 -7
  125. package/dist/workflows/worker.d.ts +15 -2
  126. package/dist/workflows/worker.d.ts.map +1 -1
  127. package/dist/workflows/worker.js +48 -4
  128. package/package.json +9 -7
  129. package/pithy.manifest.json +21 -3
  130. package/src/audit/actions.ts +9 -0
  131. package/src/capability.ts +43 -4
  132. package/src/config/config.ts +104 -5
  133. package/src/data/assignment.ts +55 -0
  134. package/src/data/embedding.ts +27 -0
  135. package/src/data/tables.ts +8 -0
  136. package/src/data/thread.ts +26 -4
  137. package/src/http/guards.ts +52 -15
  138. package/src/http/handlers.ts +125 -4
  139. package/src/http/resolve.ts +19 -0
  140. package/src/http/responses.ts +154 -0
  141. package/src/http/routes.ts +98 -12
  142. package/src/http/schemas.ts +117 -28
  143. package/src/http/scopes.ts +43 -6
  144. package/src/http/views.ts +44 -0
  145. package/src/inbound/ingest.ts +5 -2
  146. package/src/index.ts +5 -0
  147. package/src/migrations/0002_assignment.ts +65 -0
  148. package/src/migrations/0003_embeddings.ts +29 -0
  149. package/src/migrations/set.ts +21 -0
  150. package/src/presence/room.ts +94 -0
  151. package/src/provision/provisionSupport.ts +33 -1
  152. package/src/provision/resolveSupportConfig.ts +24 -13
  153. package/src/purge/contribution.ts +294 -0
  154. package/src/secret/registry.ts +17 -2
  155. package/src/semantic/documents.ts +178 -0
  156. package/src/semantic/embed.ts +99 -0
  157. package/src/semantic/messageIndex.ts +74 -0
  158. package/src/semantic/query.ts +66 -0
  159. package/src/staff/staff.ts +128 -0
  160. package/src/store/assignment.ts +179 -0
  161. package/src/store/hybrid.ts +314 -0
  162. package/src/store/paging.ts +14 -0
  163. package/src/store/provisionIndex.ts +46 -0
  164. package/src/store/search.ts +58 -37
  165. package/src/store/searchIndex.ts +4 -4
  166. package/src/store/threads.ts +91 -91
  167. package/src/submission/submit.ts +3 -2
  168. package/src/test-utils/seed.ts +141 -0
  169. package/src/test-utils/semantic.ts +110 -0
  170. package/src/version.generated.ts +1 -1
  171. package/src/workflows/specs.ts +34 -0
  172. package/src/workflows/worker.ts +84 -8
  173. package/src/workflows/wrangler.jsonc +8 -3
@@ -0,0 +1,67 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { supportSemantic } from "../config/config.js";
4
+ import { featureVectorizeIndexName } from "@pithy-sh/core/src/naming/feature";
5
+ import { resourceNames } from "@pithy-sh/core/src/naming/resourceNames";
6
+ //#region src/semantic/messageIndex.ts
7
+ /**
8
+ * The semantic message index — what it is called, what shape it has, and what text goes into it.
9
+ *
10
+ * **Support owns it, through `@pithy-sh/vector`'s public modules, and the adopter composes no `vector()`.** The
11
+ * index is declared here as a {@link VectorIndexConfig}, the same value `vector()` builds from an adopter's
12
+ * `indexes` block, so `embedForIndex`, `queryVectors` and the reprocess orchestration take it unchanged and pin
13
+ * the model on writes and queries alike.
14
+ *
15
+ * **Vectors carry ids and nothing else.** A vector's id is the message's id, and no subject, body or address
16
+ * reaches Vectorize metadata: the text stays in D1 under the privacy markers its schema carries. A match is
17
+ * resolved back to its thread in D1, which is also where every filter applies.
18
+ */
19
+ /** The index's name in `pithy vector reprocess --index`, and the `<thing>` its Cloudflare name ends with. */
20
+ const SUPPORT_MESSAGES_INDEX = "support-messages";
21
+ /** The Vectorize binding the index arrives on, in the app Worker and the classification worker alike. */
22
+ const SUPPORT_VECTORIZE_BINDING = "SUPPORT_VECTORIZE";
23
+ /** The Workers AI binding a query and an inbound message are embedded on. */
24
+ const SUPPORT_AI_BINDING = "AI";
25
+ /**
26
+ * How much of a message is embedded. A support message states its point early, and the rest is usually a
27
+ * quoted thread and a signature — text that would pull every vector toward the same neighborhood.
28
+ */
29
+ const MAX_EMBED_CHARS = 4e3;
30
+ /** The index this search config asks for, or `undefined` when `search.semantic` is off. */
31
+ function supportMessageIndex(search) {
32
+ const semantic = supportSemantic(search);
33
+ if (!semantic) return void 0;
34
+ return {
35
+ model: semantic.model,
36
+ dimensions: semantic.dimensions,
37
+ metric: "cosine",
38
+ binding: SUPPORT_VECTORIZE_BINDING
39
+ };
40
+ }
41
+ /**
42
+ * The shape `pithy support provision` creates the index with — `vector`'s own provisioning shape, so a support
43
+ * index and a `vector()` index are created, and refused on a mismatch, by the same rule. `undefined` when off.
44
+ */
45
+ function supportMessageIndexShape(search) {
46
+ const index = supportMessageIndex(search);
47
+ return index ? {
48
+ dimensions: index.dimensions,
49
+ metric: index.metric
50
+ } : void 0;
51
+ }
52
+ /**
53
+ * The index's Cloudflare name — `<project>-<env>-support-messages`, or a feature's own.
54
+ *
55
+ * Through the naming facade, which refuses a Vectorize name over 64 bytes rather than truncating it: a shortened
56
+ * name is a different, empty index.
57
+ */
58
+ function supportMessageIndexName(project, env, feature) {
59
+ if (feature) return featureVectorizeIndexName(feature, SUPPORT_MESSAGES_INDEX);
60
+ return resourceNames(project).env(env).vectorizeIndex(SUPPORT_MESSAGES_INDEX);
61
+ }
62
+ /** The text one message is embedded as: its subject, then its body, bounded. */
63
+ function embeddingText(subject, body) {
64
+ return `${subject}\n\n${body}`.slice(0, MAX_EMBED_CHARS);
65
+ }
66
+ //#endregion
67
+ export { MAX_EMBED_CHARS, SUPPORT_AI_BINDING, SUPPORT_MESSAGES_INDEX, SUPPORT_VECTORIZE_BINDING, embeddingText, supportMessageIndex, supportMessageIndexName, supportMessageIndexShape };
@@ -0,0 +1,37 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { VectorIndexConfig } from "@pithy-sh/vector/src/config/config";
4
+ import { type VectorAi } from "@pithy-sh/vector/src/embed/embed";
5
+ import { type VectorStore } from "@pithy-sh/vector/src/index/index";
6
+ import { type SupportDatabase } from "../data/tables";
7
+ import type { MessageHit } from "../store/search";
8
+ /**
9
+ * The semantic half of a search: embed the query with the index's pinned model, ask Vectorize for the nearest
10
+ * message ids, and resolve each to its thread in D1.
11
+ *
12
+ * **Ids without payload, so the ceiling is 100.** Vectorize allows 100 matches when it returns neither values
13
+ * nor metadata, and the vectors carry no metadata worth returning anyway. A thread is then scored by its best
14
+ * message, so a hundred messages roll up to the threads a page of results needs.
15
+ *
16
+ * A match whose message is gone — purged, say, before its vector was — resolves to nothing and is dropped.
17
+ */
18
+ /**
19
+ * How many message ids the semantic side asks for: Vectorize's ceiling for a payload-free query. Stated here
20
+ * rather than imported, so support reaches only `vector`'s embed, index, provisioning and reprocess modules;
21
+ * `vectorImports.test.ts` holds it to `vector`'s own constant.
22
+ */
23
+ export declare const SEMANTIC_TOP_K = 100;
24
+ /** What a semantic query needs. */
25
+ export interface SemanticQueryDeps {
26
+ /** The support tables, where each match is resolved to its thread. */
27
+ db: SupportDatabase;
28
+ /** The Workers AI binding. */
29
+ ai: VectorAi;
30
+ /** The message index's Vectorize binding. */
31
+ store: VectorStore;
32
+ /** The index's config. */
33
+ index: VectorIndexConfig;
34
+ }
35
+ /** The messages nearest in meaning to `term`, best first, each with its raw similarity. */
36
+ export declare function semanticHits(deps: SemanticQueryDeps, term: string): Promise<MessageHit[]>;
37
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/semantic/query.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAChF,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAClF,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,mCAAmC;AACnC,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,EAAE,EAAE,eAAe,CAAC;IACpB,8BAA8B;IAC9B,EAAE,EAAE,QAAQ,CAAC;IACb,6CAA6C;IAC7C,KAAK,EAAE,WAAW,CAAC;IACnB,0BAA0B;IAC1B,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,2FAA2F;AAC3F,wBAAsB,YAAY,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAwB/F"}
@@ -0,0 +1,46 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { SUPPORT_MESSAGES_TABLE } from "../data/tables.js";
4
+ import { chunkByBoundParameters } from "@pithy-sh/core/src/data/boundParameters";
5
+ import { embedForIndex } from "@pithy-sh/vector/src/embed/embed";
6
+ import { queryVectors } from "@pithy-sh/vector/src/index/index";
7
+ //#region src/semantic/query.ts
8
+ /**
9
+ * The semantic half of a search: embed the query with the index's pinned model, ask Vectorize for the nearest
10
+ * message ids, and resolve each to its thread in D1.
11
+ *
12
+ * **Ids without payload, so the ceiling is 100.** Vectorize allows 100 matches when it returns neither values
13
+ * nor metadata, and the vectors carry no metadata worth returning anyway. A thread is then scored by its best
14
+ * message, so a hundred messages roll up to the threads a page of results needs.
15
+ *
16
+ * A match whose message is gone — purged, say, before its vector was — resolves to nothing and is dropped.
17
+ */
18
+ /**
19
+ * How many message ids the semantic side asks for: Vectorize's ceiling for a payload-free query. Stated here
20
+ * rather than imported, so support reaches only `vector`'s embed, index, provisioning and reprocess modules;
21
+ * `vectorImports.test.ts` holds it to `vector`'s own constant.
22
+ */
23
+ const SEMANTIC_TOP_K = 100;
24
+ /** The messages nearest in meaning to `term`, best first, each with its raw similarity. */
25
+ async function semanticHits(deps, term) {
26
+ const [vector] = await embedForIndex(deps.ai, deps.index, [term]);
27
+ const result = await queryVectors(deps.store, deps.index, vector, { topK: 100 });
28
+ if (result.matches.length === 0) return [];
29
+ const threadOf = /* @__PURE__ */ new Map();
30
+ for (const group of chunkByBoundParameters(result.matches.map((match) => match.id), 0)) {
31
+ const rows = await deps.db.selectFrom(SUPPORT_MESSAGES_TABLE).select(["id", "threadId"]).where("id", "in", group).execute();
32
+ for (const row of rows) threadOf.set(row.id, row.threadId);
33
+ }
34
+ const hits = [];
35
+ for (const match of result.matches) {
36
+ const threadId = threadOf.get(match.id);
37
+ if (threadId !== void 0) hits.push({
38
+ messageId: match.id,
39
+ threadId,
40
+ score: match.score
41
+ });
42
+ }
43
+ return hits;
44
+ }
45
+ //#endregion
46
+ export { SEMANTIC_TOP_K, semanticHits };
@@ -0,0 +1,22 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { ActingOrganization } from "@pithy-sh/core/src/access/acting";
4
+ import type { Capability } from "@pithy-sh/core/src/capability/capability";
5
+ import type { SupportStaffConfig } from "../config/config";
6
+ /**
7
+ * Refuse, at compose, a staff block no request could be served by safely.
8
+ *
9
+ * Four refusals. **No organization**: a power is held in an organization, so with none composed nobody
10
+ * could ever be staff and the block is a mistake rather than a policy. **A power the catalog does not
11
+ * declare, or no role holds**: the same, by a typo. **A power on an assignable role**: a role is the
12
+ * catalog's and not one organization's, and an organization's own administrator may give any assignable
13
+ * role to their own members — so the power would make every customer's administrator able to read every
14
+ * other customer's support mail. **A power on the role ownership transfer confers**: unassignable, and
15
+ * still one any signed-in user can take — found an organization, nominate yourself, accept — so it opens
16
+ * the same inbox to the same people. On any other unassignable role it is conferred through the control
17
+ * plane alone, by an operator.
18
+ */
19
+ export declare function assertStaffComposition(staff: SupportStaffConfig | undefined, capabilities: readonly Capability[]): void;
20
+ /** Whether the organization this request acts for gives the caller one of support's staff powers. */
21
+ export declare function staffMay(acting: ActingOrganization | null, staff: SupportStaffConfig | undefined, power: keyof SupportStaffConfig): boolean;
22
+ //# sourceMappingURL=staff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staff.d.ts","sourceRoot":"","sources":["../../src/staff/staff.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAE3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAoD3D;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,kBAAkB,GAAG,SAAS,EACrC,YAAY,EAAE,SAAS,UAAU,EAAE,GAClC,IAAI,CA4CN;AAED,qGAAqG;AACrG,wBAAgB,QAAQ,CACtB,MAAM,EAAE,kBAAkB,GAAG,IAAI,EACjC,KAAK,EAAE,kBAAkB,GAAG,SAAS,EACrC,KAAK,EAAE,MAAM,kBAAkB,GAC9B,OAAO,CAET"}
@@ -0,0 +1,64 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { InternalError } from "@pithy-sh/core/src/error/pithyError";
4
+ //#region src/staff/staff.ts
5
+ /** A composed capability's role catalog, read by shape, or undefined when it carries none. */
6
+ function catalogOf(capability) {
7
+ const catalog = capability.organizationRoles;
8
+ if (!catalog || !Array.isArray(catalog.powers) || !Array.isArray(catalog.roles) || !Array.isArray(catalog.assignableRoles) || typeof catalog.roleAllows !== "function") return;
9
+ return catalog;
10
+ }
11
+ /**
12
+ * The role a composed ownership transfer confers, read by shape, or undefined where there is no transfer.
13
+ *
14
+ * Unassignable, always — organization refuses a transfer onto an assignable role — and still self-served:
15
+ * anybody may found an organization, every new organization starts with no holder, and with no holder an
16
+ * administrator may nominate themselves and accept. So an unassignable role is out of an organization's
17
+ * own reach only when it is not this one.
18
+ */
19
+ function ownershipRoleOf(capability) {
20
+ const confers = capability.organizationOwnership?.confers;
21
+ return typeof confers === "string" ? confers : void 0;
22
+ }
23
+ /** Refuse a staff composition at boot, naming what is wrong and what to change. */
24
+ function refuse(message, action, detail) {
25
+ throw new InternalError({
26
+ message,
27
+ action,
28
+ detail
29
+ });
30
+ }
31
+ /**
32
+ * Refuse, at compose, a staff block no request could be served by safely.
33
+ *
34
+ * Four refusals. **No organization**: a power is held in an organization, so with none composed nobody
35
+ * could ever be staff and the block is a mistake rather than a policy. **A power the catalog does not
36
+ * declare, or no role holds**: the same, by a typo. **A power on an assignable role**: a role is the
37
+ * catalog's and not one organization's, and an organization's own administrator may give any assignable
38
+ * role to their own members — so the power would make every customer's administrator able to read every
39
+ * other customer's support mail. **A power on the role ownership transfer confers**: unassignable, and
40
+ * still one any signed-in user can take — found an organization, nominate yourself, accept — so it opens
41
+ * the same inbox to the same people. On any other unassignable role it is conferred through the control
42
+ * plane alone, by an operator.
43
+ */
44
+ function assertStaffComposition(staff, capabilities) {
45
+ if (!staff) return;
46
+ const tenancy = capabilities.find((candidate) => candidate.name === "organization");
47
+ const catalog = tenancy ? catalogOf(tenancy) : void 0;
48
+ if (!catalog) refuse("support() names staff powers, and organization is not composed.", "Compose organization() with a role catalog that declares them, or remove `staff` from support().", "a staff power is held in an organization; with none composed nobody could ever hold one");
49
+ const ownership = tenancy ? ownershipRoleOf(tenancy) : void 0;
50
+ for (const power of [staff.readPower, staff.assignPower]) {
51
+ if (!catalog.powers.includes(power)) refuse(`support() names staff power \`${power}\`, which your role catalog does not declare.`, "Declare it in defineRoles({ powers }) and give it to an unassignable role your staff hold, other than the one ownership transfer confers.", `declared powers: ${catalog.powers.join(", ")}`);
52
+ const holders = catalog.roles.filter((role) => catalog.roleAllows(role, power));
53
+ if (holders.length === 0) refuse(`No role in your catalog holds support's staff power \`${power}\`.`, "Give it to an unassignable role your staff hold, other than the one ownership transfer confers.", `power ${power} is declared and held by no role`);
54
+ const handable = holders.filter((role) => catalog.assignableRoles.includes(role));
55
+ if (handable.length > 0) refuse(`Support's staff power \`${power}\` is on a role an organization may hand out: ${handable.join(", ")}.`, "Move it to a role listed in defineRoles({ unassignable }) that ownership transfer does not confer, so only an operator can confer it.", `an organization administrator may assign ${handable.join(", ")} to their own members, which would open every customer's support mail to them`);
56
+ if (ownership !== void 0 && holders.includes(ownership)) refuse(`Support's staff power \`${power}\` is on \`${ownership}\`, the role ownership transfer confers, which an organization's founder may take alone.`, "Move it to a role listed in defineRoles({ unassignable }) that ownership transfer does not confer, so only an operator can confer it.", `anybody may found an organization and nominate themselves as its ${ownership}, which would open every customer's support mail to them`);
57
+ }
58
+ }
59
+ /** Whether the organization this request acts for gives the caller one of support's staff powers. */
60
+ function staffMay(acting, staff, power) {
61
+ return acting !== null && staff !== void 0 && acting.roleAllows(staff[power]);
62
+ }
63
+ //#endregion
64
+ export { assertStaffComposition, staffMay };
@@ -0,0 +1,63 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { D1Database } from "@cloudflare/workers-types";
4
+ import { SupportAssignment, type SupportAssignmentActorType } from "../data/assignment";
5
+ import { type SupportDatabase } from "../data/tables";
6
+ import { SupportThread } from "../data/thread";
7
+ /**
8
+ * Who has a thread (#816).
9
+ *
10
+ * **One fact, not a state machine.** A thread has an assignee or it has none, and any change moves it
11
+ * straight to the next one. Every real change appends a row to `pithy_support_assignments`; a change that
12
+ * changes nothing — the current assignee again, or unassigning a thread nobody has — writes nothing at
13
+ * all, which is what lets a caller emit its audit event only when something happened.
14
+ *
15
+ * **Compare-and-set, not last-write-wins on a read.** The previous assignee is part of what the history and
16
+ * the audit event record, so the write is conditioned on the assignee this call read. Two handovers racing
17
+ * from one state cannot both claim to have replaced the same person: the loser re-reads and replaces
18
+ * whoever won, and the chain of `previousRef` stays unbroken.
19
+ *
20
+ * **The change and its history row land together or not at all.** The conditional update and the history
21
+ * insert go to D1 as one batch, which D1 runs as one transaction, and the insert is guarded on the update
22
+ * having changed a row. A history row that cannot be written rolls the assignee back with it, so a retry
23
+ * sees the old assignee, makes the change again, and records it — rather than finding the change already
24
+ * made, taking the no-op branch, and leaving that handover out of the history and the audit trail for good.
25
+ */
26
+ /** Somebody a thread may be assigned to: an opaque reference and how to show it. */
27
+ export interface Assignee {
28
+ /** Opaque — a dashboard member, a user of the app, an agent. Support never interprets it. */
29
+ ref: string;
30
+ /** Display text, as the assigner named them. */
31
+ name: string;
32
+ }
33
+ /** What {@link assignThread} takes. */
34
+ export interface AssignThreadOptions {
35
+ /** The thread. */
36
+ threadId: string;
37
+ /** Who should have it, or null for nobody. */
38
+ assignee: Assignee | null;
39
+ /** How the assigner was verified. */
40
+ actorType: SupportAssignmentActorType;
41
+ /** The control-plane subject, or the signed-in user's id. */
42
+ assignedBy: string;
43
+ /** Why, when the assigner said. */
44
+ note?: string;
45
+ /** Generate a history row id. */
46
+ newId: () => string;
47
+ /** Now. */
48
+ now: Date;
49
+ }
50
+ /** What {@link assignThread} did. */
51
+ export interface AssignThreadResult {
52
+ /** The thread as it now stands. */
53
+ thread: SupportThread;
54
+ /** Whether anything changed. False for the current assignee again, or for unassigning nobody. */
55
+ changed: boolean;
56
+ /** Who had it before this call, or null when nobody did. */
57
+ previous: Assignee | null;
58
+ }
59
+ /** Assign a thread, or unassign it. Throws `support/not_found` when it does not exist. */
60
+ export declare function assignThread(d1: D1Database, options: AssignThreadOptions): Promise<AssignThreadResult>;
61
+ /** A thread's assignee history, oldest first. */
62
+ export declare function listAssignments(db: SupportDatabase, threadId: string): Promise<SupportAssignment[]>;
63
+ //# sourceMappingURL=assignment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../../src/store/assignment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,2BAA2B,CAAC;AAGjF,OAAO,EAAE,iBAAiB,EAAE,KAAK,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AACxF,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,oFAAoF;AACpF,MAAM,WAAW,QAAQ;IACvB,6FAA6F;IAC7F,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC,kBAAkB;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,qCAAqC;IACrC,SAAS,EAAE,0BAA0B,CAAC;IACtC,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,WAAW;IACX,GAAG,EAAE,IAAI,CAAC;CACX;AAED,qCAAqC;AACrC,MAAM,WAAW,kBAAkB;IACjC,mCAAmC;IACnC,MAAM,EAAE,aAAa,CAAC;IACtB,iGAAiG;IACjG,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3B;AAiCD,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA6D5G;AAED,iDAAiD;AACjD,wBAAsB,eAAe,CAAC,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAUzG"}
@@ -0,0 +1,86 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { SupportNotFoundError } from "../error/errors.js";
4
+ import { SupportAssignment } from "../data/assignment.js";
5
+ import { SupportThread } from "../data/thread.js";
6
+ import { SUPPORT_ASSIGNMENTS_TABLE, SUPPORT_THREADS_TABLE, supportDatabase } from "../data/tables.js";
7
+ import { InternalError } from "@pithy-sh/core/src/error/pithyError";
8
+ import { sql } from "kysely";
9
+ //#region src/store/assignment.ts
10
+ /** How many times a write lost to a concurrent one is retried before the request gives up. */
11
+ const MAX_ATTEMPTS = 5;
12
+ /** The assignee a thread row names, or null. */
13
+ function assigneeOf(thread) {
14
+ return thread.assigneeRef ? {
15
+ ref: thread.assigneeRef,
16
+ name: thread.assigneeName ?? thread.assigneeRef
17
+ } : null;
18
+ }
19
+ /** A compiled Kysely query as a D1 statement, for a batch. */
20
+ function prepared(d1, compiled) {
21
+ return d1.prepare(compiled.sql).bind(...compiled.parameters);
22
+ }
23
+ /**
24
+ * Insert a history row only when the statement before it changed a row — `changes()` answers for the
25
+ * statement just before it in the batch — so a compare-and-set that matched nothing writes no history.
26
+ */
27
+ function guardedHistory(db, row) {
28
+ const columns = Object.keys(row);
29
+ return db.insertInto(SUPPORT_ASSIGNMENTS_TABLE).columns(columns).expression(db.selectNoFrom(columns.map((column) => sql.val(row[column]).as(column))).where(sql`changes() > 0`)).compile();
30
+ }
31
+ /** Assign a thread, or unassign it. Throws `support/not_found` when it does not exist. */
32
+ async function assignThread(d1, options) {
33
+ const db = supportDatabase(d1);
34
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
35
+ const row = await db.selectFrom(SUPPORT_THREADS_TABLE).selectAll().where("id", "=", options.threadId).executeTakeFirst();
36
+ if (!row) throw new SupportNotFoundError({ detail: `no support thread ${options.threadId}` });
37
+ const current = SupportThread.parse(row);
38
+ const previous = assigneeOf(current);
39
+ if ((previous?.ref ?? null) === (options.assignee?.ref ?? null)) return {
40
+ thread: current,
41
+ changed: false,
42
+ previous
43
+ };
44
+ const now = options.now.getTime();
45
+ const next = {
46
+ assigneeRef: options.assignee?.ref ?? null,
47
+ assigneeName: options.assignee?.name ?? null,
48
+ assignedAt: options.assignee ? now : null,
49
+ assignedBy: options.assignee ? options.assignedBy : null,
50
+ updatedAt: now
51
+ };
52
+ const update = db.updateTable(SUPPORT_THREADS_TABLE).set(next).where("id", "=", options.threadId).where("assigneeRef", "is", previous?.ref ?? null).compile();
53
+ const history = guardedHistory(db, SupportAssignment.encode({
54
+ id: options.newId(),
55
+ threadId: options.threadId,
56
+ assigneeRef: options.assignee?.ref ?? null,
57
+ assigneeName: options.assignee?.name ?? null,
58
+ previousRef: previous?.ref ?? null,
59
+ previousName: previous?.name ?? null,
60
+ actorType: options.actorType,
61
+ assignedBy: options.assignedBy,
62
+ note: options.note ?? null,
63
+ createdAt: options.now
64
+ }));
65
+ const [updated] = await d1.batch([prepared(d1, update), prepared(d1, history)]);
66
+ if ((updated?.meta.changes ?? 0) === 0) continue;
67
+ return {
68
+ thread: SupportThread.parse({
69
+ ...row,
70
+ ...next
71
+ }),
72
+ changed: true,
73
+ previous
74
+ };
75
+ }
76
+ throw new InternalError({
77
+ message: "That conversation is being reassigned by somebody else. Try again.",
78
+ detail: `assignment of thread ${options.threadId} lost ${MAX_ATTEMPTS} races in a row`
79
+ });
80
+ }
81
+ /** A thread's assignee history, oldest first. */
82
+ async function listAssignments(db, threadId) {
83
+ return (await db.selectFrom(SUPPORT_ASSIGNMENTS_TABLE).selectAll().where("threadId", "=", threadId).orderBy("createdAt", "asc").orderBy(sql`rowid`, "asc").execute()).map((row) => SupportAssignment.parse(row));
84
+ }
85
+ //#endregion
86
+ export { assignThread, listAssignments };
@@ -0,0 +1,134 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { Logger } from "@pithy-sh/core/src/logger/logger";
4
+ import type { VectorIndexConfig } from "@pithy-sh/vector/src/config/config";
5
+ import type { VectorAi } from "@pithy-sh/vector/src/embed/embed";
6
+ import type { VectorStore } from "@pithy-sh/vector/src/index/index";
7
+ import { type SupportDatabase } from "../data/tables";
8
+ import { type KeywordBackend, type MessageHit } from "./search";
9
+ import { type ListedThread, type ThreadFilters } from "./threads";
10
+ /**
11
+ * Hybrid search — keyword and semantic, run together and ranked as one.
12
+ *
13
+ * The two fail in opposite directions. Keyword finds the exact string and misses the paraphrase; semantic
14
+ * finds the paraphrase and misses the identifier, because it cannot retrieve an order number it has never
15
+ * seen. A support inbox needs both.
16
+ *
17
+ * **Search is not listing.** A query goes in, ranked threads come out, up to a limit, and there is no cursor:
18
+ * a ranked list has a top, not pages.
19
+ *
20
+ * ## How a result is ranked
21
+ *
22
+ * 1. Each backend returns **messages**, best first — FTS5 by `bm25`, the `LIKE` scan by recency, Vectorize by
23
+ * similarity.
24
+ * 2. Each list rolls up to **threads**: a thread takes the rank of its best message, and appears once.
25
+ * 3. Hybrid fuses the two thread lists by **reciprocal rank fusion** over rank position — `1 / (60 + rank)`,
26
+ * summed. No weights to tune, and a thread both backends found rises above one only one found. The raw
27
+ * scores are never normalized into one number: each result carries its message's `bm25` and similarity as
28
+ * they were, absent where that backend did not run or did not match.
29
+ * 4. A tie goes to the keyword hit, the better keyword rank first, and then to `lastMessageAt`, newest first.
30
+ * Semantic search cannot retrieve an identifier, so for a query like `ORD-40912` the exact match is keyword's
31
+ * rank 1 while semantic's rank 1 is some other thread: equal fused scores, and the exact match wins.
32
+ *
33
+ * **Filters apply after retrieval, in D1**, never as Vectorize metadata, which would go stale the moment a
34
+ * thread is archived, reassigned or reclassified. A narrow filter can therefore return fewer than the limit,
35
+ * and the result's count says how many it did.
36
+ *
37
+ * **Degrade, never fail.** A backend that is on and cannot answer — FTS5 configured and its table not
38
+ * provisioned, the semantic index or its binding absent, a model that does not answer — is skipped and named,
39
+ * and the rest of the search still runs.
40
+ */
41
+ /** The search modes a caller may ask for. */
42
+ export type SearchMode = "keyword" | "semantic" | "hybrid";
43
+ /** A backend that can answer part of a search. */
44
+ export type SearchBackend = KeywordBackend | "semantic";
45
+ /** Why a backend that is on did not answer. */
46
+ export type SkipReason = "unprovisioned" | "failed";
47
+ /** The reciprocal-rank-fusion constant — the standard 60, which damps the head without flattening it. */
48
+ export declare const RRF_K = 60;
49
+ /** How many messages each backend retrieves before the roll-up. Vectorize's payload-free ceiling. */
50
+ export declare const SEARCH_HITS = 100;
51
+ /** One thread a backend ranked, with the message that ranked it. */
52
+ export interface RankedThread {
53
+ /** The thread. */
54
+ threadId: string;
55
+ /** Its best-matching message. */
56
+ messageId: string;
57
+ /** Its rank in this backend's list, 1 first. */
58
+ rank: number;
59
+ }
60
+ /**
61
+ * Roll a backend's message hits up to threads: a thread takes the rank of its best message, and appears once.
62
+ * The input is best first, so the first message seen for a thread is its best.
63
+ */
64
+ export declare function rollUp(hits: readonly MessageHit[]): RankedThread[];
65
+ /** One thread after fusion: its fused score and the message the result names. */
66
+ export interface FusedThread {
67
+ /** The thread. */
68
+ threadId: string;
69
+ /** The message the result names — the one from the backend that ranked this thread higher. */
70
+ messageId: string;
71
+ /** The reciprocal-rank-fusion score. Internal: it orders results and is never reported. */
72
+ fused: number;
73
+ }
74
+ /**
75
+ * Fuse ranked thread lists by reciprocal rank fusion. A thread found by several lists sums its terms.
76
+ *
77
+ * The message a result names comes from the list that ranked the thread highest, the first list winning a
78
+ * tie — the keyword list is passed first, so an exact match names the message that holds it.
79
+ */
80
+ export declare function fuse(lists: readonly (readonly RankedThread[])[]): FusedThread[];
81
+ /** What a search asks. */
82
+ export interface SearchThreadsQuery extends ThreadFilters {
83
+ /** The words. */
84
+ q: string;
85
+ /** Which backends to run. Absent runs every backend that is on. */
86
+ mode?: SearchMode;
87
+ /** How many threads to return. Defaults to {@link DEFAULT_SEARCH_LIMIT}, capped at {@link MAX_SEARCH_LIMIT}. */
88
+ limit?: number;
89
+ }
90
+ /** What a search needs. The semantic seams are absent exactly when that backend cannot run here. */
91
+ export interface SearchDeps {
92
+ /** The support tables. */
93
+ db: SupportDatabase;
94
+ /** Whether `search.fts` is on. */
95
+ fts: boolean;
96
+ /** The message index, when `search.semantic` is on. */
97
+ index?: VectorIndexConfig;
98
+ /** The Workers AI binding, when bound. */
99
+ ai?: VectorAi;
100
+ /** The message index's Vectorize binding, when bound. */
101
+ store?: VectorStore;
102
+ /** The calling viewer — whose read and snooze state each result carries. */
103
+ viewer: string;
104
+ /** Where a skipped backend is reported to the operator. */
105
+ log?: Logger;
106
+ }
107
+ /** One search result: a thread, and the message that matched best with its raw scores. */
108
+ export interface SearchResult {
109
+ /** The thread, as this viewer sees it. */
110
+ thread: ListedThread;
111
+ /** The best-matching message. */
112
+ messageId: string;
113
+ /** That message's FTS5 `bm25`, raw — lower is better. Absent when FTS5 did not run or did not match it. */
114
+ bm25?: number;
115
+ /** That message's similarity to the query, raw. Absent when semantic search did not run or did not match it. */
116
+ similarity?: number;
117
+ }
118
+ /** What a search did, and what it found. */
119
+ export interface SearchOutcome {
120
+ /** The mode that ran — the one asked for, or `hybrid` when none was. */
121
+ mode: SearchMode;
122
+ /** The backends that answered. */
123
+ ran: SearchBackend[];
124
+ /** The backends that were on and could not answer, and why. */
125
+ skipped: {
126
+ backend: SearchBackend;
127
+ reason: SkipReason;
128
+ }[];
129
+ /** The threads, best first. */
130
+ results: SearchResult[];
131
+ }
132
+ /** Run a search. */
133
+ export declare function searchThreads(deps: SearchDeps, query: SearchThreadsQuery): Promise<SearchOutcome>;
134
+ //# sourceMappingURL=hybrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../src/store/hybrid.ts"],"names":[],"mappings":"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"}