@opencxh/domain 1.211.0 → 1.214.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.
@@ -36,6 +36,10 @@ export declare const RESOURCE_RESOLVE_SERVICE = "resources.resolve";
36
36
  * items carry the conversation's key, the conversation knows nothing about work. Answering it
37
37
  * here rather than per consumer is what keeps the shell from growing an
38
38
  * `if (kind === "interaction")` per app that could be linked.
39
+ *
40
+ * The rows may be {@link RelatedResource}s, i.e. carry an `origin`. Answering with the plain
41
+ * summary stays valid; a consumer that wants to separate a link from a lead reads that field and
42
+ * treats its absence as "unknown, so not a stated link".
39
43
  */
40
44
  export declare const RESOURCE_ATTACHED_SERVICE = "resources.attached";
41
45
  /** One pointable resource, as the app that owns it shows it. */
@@ -106,3 +110,104 @@ export interface ResourceAttachedParams {
106
110
  * and this contract is read mostly on the client.
107
111
  */
108
112
  export declare function resourceKindOf(scopeKey: string): string;
113
+ /**
114
+ * The same question, server-side: **what hangs on this key, and is that a fact or a guess.**
115
+ *
116
+ * The service-bus keys above are asked by a user with the app open. An AI tool, a playbook and a
117
+ * job have no client and no store, so they need the question over HTTP, and in one place: the hub
118
+ * in apps/context (`POST /resource/related`). It answers from two sources:
119
+ *
120
+ * - **linked** — a {@link Relation} row: somebody (or a tool) linked the two. One local indexed
121
+ * read on the relation store, both directions, no protocol between apps.
122
+ * - **derived** — a row that merely shares a dossier key with the anchor (`domain:`, `mailto:`).
123
+ * The hub fans `/provider/scope/related` out over the **existing `scope` provider role**: the
124
+ * apps that own kinds already authorize there and already return {@link ScopeAuth.keys}.
125
+ *
126
+ * | Route | Body | Answer |
127
+ * |---|---|---|
128
+ * | `/provider/scope/related` | {@link RelatedResourceRequest} | {@link RelatedResourceResponse} |
129
+ * | `/provider/scope/read` | {@link ResourceReadRequest} | {@link ResourceReadResponse} |
130
+ *
131
+ * Both are app-to-app and optional: an app that does not implement them loses its own rows, never
132
+ * the answer. The hub resolves the anchor's dossier keys **once** (`authorizeIdentity`) and passes
133
+ * them along, so a fan-out over five apps still costs one authorization.
134
+ */
135
+ export declare const SCOPE_RELATED_ROUTE = "/provider/scope/related";
136
+ export declare const SCOPE_READ_ROUTE = "/provider/scope/read";
137
+ /**
138
+ * One explicit link between two resources, stored by apps/context.
139
+ *
140
+ * **Two rows per link**, out and back, so "what hangs on this key" is one indexed query on
141
+ * `fromKey` in either direction instead of an `$or` no index serves. **One hop, never traversed:**
142
+ * this store cannot join or recurse, so a relation is read, not followed. Party matches (a shared
143
+ * domain, a shared address) are not relations; they stay dossier keys on the row.
144
+ */
145
+ export interface Relation {
146
+ id: string;
147
+ organizationId: string;
148
+ fromKey: string;
149
+ toKey: string;
150
+ /**
151
+ * The relation and the party role in one vocabulary: `client` · `opposing` · `context` ·
152
+ * `blocks` · `blocked_by` · `relates` · `duplicates` · `attachment` · `party`. One field, no
153
+ * separate role column: "this company is the client" and "this item blocks that" are both a
154
+ * named arrow.
155
+ */
156
+ type: string;
157
+ /** Shared by the outbound and the inbound row; keeps unlinking one action. */
158
+ pairId: string;
159
+ /** How the inbound row identifies itself, so a UI can show "is blocked by". */
160
+ inverse?: boolean;
161
+ createdBy: string;
162
+ createdAt?: number;
163
+ }
164
+ /**
165
+ * What the hub asks one app. `scopeKey` is already authorized — the answering app must not
166
+ * re-gate it, but it must filter its own rows for the stamped acting identity.
167
+ */
168
+ export interface RelatedResourceRequest {
169
+ /** The anchor: the thing the user (or the model) is standing on. */
170
+ scopeKey: string;
171
+ /**
172
+ * The anchor's dossier keys and the keys it is linked to, `scopeKey` included. An app matches
173
+ * its own indexed key column against this in one `$in`, and resolves the keys that name its
174
+ * own rows (`interaction:<id>`).
175
+ */
176
+ keys: string[];
177
+ /** Maximum rows this app may return. Absent = its own sensible cap. */
178
+ limit?: number;
179
+ }
180
+ /**
181
+ * One related row, and **why** it is related.
182
+ *
183
+ * `origin` is decided by the hub: a row the anchor has a {@link Relation} to is `linked`, any
184
+ * other match is `derived`. An app sets it only for a **structural** relation it owns and that
185
+ * no relation row expresses (a contact's employer via `companyId`). A consumer keeps the two
186
+ * apart: a model told "these are the linked conversations" reads a domain match as a fact.
187
+ */
188
+ export interface RelatedResource extends ResourceSummary {
189
+ origin?: "linked" | "derived";
190
+ /** Why this row is here: the relation type (`client`), or the dossier key that matched. */
191
+ via?: string;
192
+ }
193
+ export interface RelatedResourceResponse {
194
+ /**
195
+ * Rows relevant to the asked key set, in both directions an app can answer:
196
+ * rows **identified by** one of the keys (resolution), and rows **carrying** one of them
197
+ * (the key match). One route for both, because to the answering app they are the same
198
+ * `$in` over the same indexed column.
199
+ */
200
+ rows: RelatedResource[];
201
+ }
202
+ /** Params for `/provider/scope/read`: one key, answered by the app that owns the kind. */
203
+ export interface ResourceReadRequest {
204
+ scopeKey: string;
205
+ }
206
+ /**
207
+ * One resource as readable text — the generic counterpart of `read_work_item` /
208
+ * `list_activities`, so a caller with a bare key does not have to know which app's tool reads it.
209
+ */
210
+ export interface ResourceReadResponse {
211
+ text: string;
212
+ title?: string;
213
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -11,3 +11,23 @@ export declare const SUMMARY_MIN_MESSAGES = 3;
11
11
  export declare const SUMMARY_MIN_LAST_CHARS = 600;
12
12
  /** True when the thread is long enough that a summary saves the reader scrolling. */
13
13
  export declare function isThreadLongEnough(messageCount: number, lastMessageChars: number): boolean;
14
+ /** Headers that say something about bulk/automated origin. Nothing else is stored. */
15
+ export declare const MAIL_HINT_HEADERS: string[];
16
+ /** Graph and Gmail both hand us `{name,value}[]`; keep only the whitelist, lowercased. */
17
+ export declare function pickMailHeaders(entries: {
18
+ name?: string;
19
+ value?: string;
20
+ }[]): Record<string, string>;
21
+ export type MailClass = "marketing" | "automated";
22
+ /**
23
+ * Which bulk label an inbound mail deserves, if any. Headers first, sender/body
24
+ * regex as fallback. `automated` is checked first on purpose: an order
25
+ * confirmation carrying an unsubscribe footer is still automated, not marketing.
26
+ */
27
+ export declare function classifyMail(p: {
28
+ from?: {
29
+ email: string;
30
+ };
31
+ body?: string;
32
+ headers?: Record<string, string>;
33
+ }): MailClass | undefined;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencxh/domain",
3
- "version": "1.211.0",
3
+ "version": "1.214.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",