@opencxh/domain 1.172.1 → 1.173.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/dist/entities/activity/blocks.d.ts +30 -30
- package/dist/entities/activity/catalog.d.ts +63 -86
- package/dist/entities/activity/descriptor.d.ts +31 -32
- package/dist/entities/activity/index.d.ts +2 -2
- package/dist/entities/activity/preview.d.ts +7 -7
- package/dist/entities/activity/resolve.d.ts +37 -38
- package/dist/entities/activity/types.d.ts +64 -35
- package/dist/entities/ai-account/types.d.ts +3 -3
- package/dist/entities/ai-account/vendors.d.ts +25 -32
- package/dist/entities/ai-budget/types.d.ts +5 -5
- package/dist/entities/ai-conversation/types.d.ts +12 -12
- package/dist/entities/ai-profile/types.d.ts +14 -21
- package/dist/entities/ai-settings/types.d.ts +12 -13
- package/dist/entities/analytics/index.d.ts +4 -4
- package/dist/entities/analytics/source.d.ts +1 -1
- package/dist/entities/analytics/usage.d.ts +23 -28
- package/dist/entities/artifact/blocks.d.ts +54 -69
- package/dist/entities/artifact/markdown.d.ts +2 -3
- package/dist/entities/artifact/types.d.ts +46 -54
- package/dist/entities/assignment/types.d.ts +76 -90
- package/dist/entities/channel/index.d.ts +1 -1
- package/dist/entities/channel/signature.d.ts +8 -9
- package/dist/entities/communication/index.d.ts +1 -1
- package/dist/entities/contact/index.d.ts +1 -1
- package/dist/entities/custom-field-def/types.d.ts +14 -14
- package/dist/entities/external-identity/types.d.ts +45 -50
- package/dist/entities/interaction/index.d.ts +1 -1
- package/dist/entities/interaction/types.d.ts +44 -47
- package/dist/entities/live-lens/types.d.ts +39 -44
- package/dist/entities/mcp/types.d.ts +53 -57
- package/dist/entities/memory/alias.d.ts +7 -7
- package/dist/entities/memory/browse.d.ts +21 -21
- package/dist/entities/memory/ingest.d.ts +24 -28
- package/dist/entities/memory/item.d.ts +53 -58
- package/dist/entities/memory/kind.d.ts +25 -25
- package/dist/entities/memory/query.d.ts +49 -51
- package/dist/entities/playbook/actor.d.ts +17 -17
- package/dist/entities/playbook/assignment.d.ts +54 -63
- package/dist/entities/playbook/index.d.ts +2 -2
- package/dist/entities/playbook/labels.d.ts +4 -4
- package/dist/entities/playbook/trigger-vars.d.ts +17 -15
- package/dist/entities/playbook/types.d.ts +174 -303
- package/dist/entities/resource-reminder/types.d.ts +8 -9
- package/dist/entities/time-entry/duration.d.ts +11 -11
- package/dist/entities/time-entry/types.d.ts +59 -59
- package/dist/entities/time-entry/work-type.d.ts +21 -24
- package/dist/entities/topic/examples.d.ts +26 -27
- package/dist/entities/topic/scope.d.ts +11 -12
- package/dist/entities/topic/types.d.ts +26 -28
- package/dist/entities/user/types.d.ts +24 -28
- package/dist/entities/webhook/types.d.ts +6 -6
- package/dist/entities/work/activity.d.ts +36 -0
- package/dist/entities/work/index.d.ts +1 -0
- package/dist/entities/work/keys.d.ts +25 -26
- package/dist/entities/work/ladder.d.ts +48 -52
- package/dist/entities/work/types.d.ts +108 -114
- package/dist/index.cjs +6 -6
- package/dist/index.d.ts +29 -28
- package/dist/index.js +1416 -1258
- package/dist/platform/account.d.ts +34 -38
- package/dist/platform/ai-tools.d.ts +48 -52
- package/dist/platform/api.d.ts +9 -10
- package/dist/platform/author.d.ts +7 -9
- package/dist/platform/capabilities.d.ts +3 -3
- package/dist/platform/communication.d.ts +163 -204
- package/dist/platform/context.d.ts +8 -8
- package/dist/platform/identity.d.ts +16 -19
- package/dist/platform/kernel.d.ts +1 -1
- package/dist/platform/manifest.d.ts +2 -2
- package/dist/platform/media.d.ts +13 -15
- package/dist/platform/permission.d.ts +39 -0
- package/dist/platform/permission.test.d.ts +1 -0
- package/dist/platform/presence.d.ts +30 -30
- package/dist/platform/provider.d.ts +14 -15
- package/dist/platform/resource-source.d.ts +43 -45
- package/dist/platform/resource.d.ts +17 -23
- package/dist/platform/scope.d.ts +35 -61
- package/dist/platform/services.d.ts +4 -4
- package/dist/platform/storage.d.ts +1 -1
- package/dist/platform/sync-source.d.ts +196 -216
- package/dist/platform/transcript-cadence.d.ts +37 -41
- package/dist/platform/ui.d.ts +6 -6
- package/dist/text/endpoint.d.ts +11 -27
- package/package.json +3 -2
|
@@ -1,150 +1,136 @@
|
|
|
1
1
|
import { ResourceRef } from '../../platform/resource';
|
|
2
|
-
import {
|
|
2
|
+
import { FeedActivity } from '../activity/types';
|
|
3
3
|
import { WaitingOn } from '../playbook/types';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* An **assignment**: work handed to an agent, living as long as the work does.
|
|
6
6
|
*
|
|
7
|
-
* **
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* **Why this is an entity.** It was not one — an assignment used to be a run with a procedure, an
|
|
8
|
+
* agent as actor and a long lifetime. Defensible, and the code showed what it cost: every
|
|
9
|
+
* procedure run was cut short at `{ status: "done" }`, so an assignment was structurally **one
|
|
10
|
+
* model turn**; the thread shared no memory with the agent, so what a person wrote in it never
|
|
11
|
+
* reached the agent and the other way round; `retry` meant "redo the whole assignment from the
|
|
12
|
+
* original trigger", sensible for a workflow of seconds and nonsense for work running for days;
|
|
13
|
+
* and "what kind of thing is this" had to be derived from the execution shape.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
* structureel **één modelbeurt**;
|
|
13
|
-
* - de draad (een `AIConversation` op `playbook-run:<id>`) deelde géén geheugen met de agent:
|
|
14
|
-
* `runProcedure` gaf bewust geen `conversationId` mee, dus wat een mens in de draad zei bereikte
|
|
15
|
-
* de agent nooit en omgekeerd;
|
|
16
|
-
* - `retry` betekende "doe de hele opdracht opnieuw vanaf de originele trigger" — zinnig voor een
|
|
17
|
-
* workflow van seconden, onzin voor werk dat dagen liep;
|
|
18
|
-
* - en "wat voor ding is dit" moest uit de *uitvoeringsvorm* worden afgeleid
|
|
19
|
-
* (`decider.kind === "procedure"`), dus de vorm deed dienst als producttype.
|
|
20
|
-
*
|
|
21
|
-
* Nu is een opdracht wat hij is, en een run wat die is: **één beurt**. De opdracht leeft, de runs
|
|
22
|
-
* zijn wat hij deed.
|
|
15
|
+
* Now an assignment is what it is, and a run is what it is: **one turn**.
|
|
23
16
|
*/
|
|
24
17
|
/**
|
|
25
|
-
*
|
|
18
|
+
* The state an assignment is in.
|
|
26
19
|
*
|
|
27
|
-
*
|
|
28
|
-
* `failed
|
|
29
|
-
*
|
|
20
|
+
* Deliberately the same words as a run's end states, and deliberately fewer: an assignment cannot
|
|
21
|
+
* be `failed`. A failed turn is a failed run — the assignment is then still open, because nothing
|
|
22
|
+
* was handled. That difference is exactly why this is its own vocabulary.
|
|
30
23
|
*/
|
|
31
24
|
export type AssignmentStatus = "open" | "waiting" | "done" | "escalated";
|
|
32
25
|
export declare const TERMINAL_ASSIGNMENT_STATUSES: readonly ["done", "escalated"];
|
|
33
26
|
export declare function isAssignmentTerminal(status: AssignmentStatus | string): boolean;
|
|
34
27
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* Was `playbook-run:<id>`, want de draad hing aan een run. Hij hangt nu aan de opdracht, en dat is
|
|
38
|
-
* het hele punt: een draad die stopt zodra één beurt klaar is, is geen draad.
|
|
28
|
+
* The scope kind an assignment's thread lives under. It hangs on the assignment, not on a
|
|
29
|
+
* single run — a thread that stops when one turn finishes is no thread.
|
|
39
30
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
31
|
+
* In domain because it is a **contract between client and server**: the client builds the key to
|
|
32
|
+
* open the thread, the server authorizes it. Two places spelling the prefix out separately drift
|
|
33
|
+
* apart silently on the first typo — and the result of that is "no access to this scope", not an
|
|
34
|
+
* error message saying so.
|
|
44
35
|
*/
|
|
45
36
|
export declare const ASSIGNMENT_SCOPE_KIND = "assignment";
|
|
46
37
|
export declare function assignmentScopeKey(assignmentId: string): string;
|
|
47
38
|
export interface Assignment {
|
|
48
39
|
id: string;
|
|
49
40
|
organizationId: string;
|
|
50
|
-
/**
|
|
41
|
+
/** The agent doing this: the `userId` of a user with `type: "agent"`. */
|
|
51
42
|
agentId: string;
|
|
52
|
-
/**
|
|
43
|
+
/** The definition this assignment came from (a playbook with a procedure). */
|
|
53
44
|
jobId: string;
|
|
54
45
|
/**
|
|
55
|
-
*
|
|
46
|
+
* The version it started on — purely for the audit.
|
|
56
47
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
48
|
+
* Every **turn** pins its own procedure (`run.decider`), so improved prose arrives on the next
|
|
49
|
+
* turn without a running turn switching instructions mid-flight. Pinning forever would mean a
|
|
50
|
+
* correction never arrives; not pinning would mean a turn changes halfway.
|
|
60
51
|
*/
|
|
61
52
|
jobVersion: number;
|
|
62
|
-
/**
|
|
53
|
+
/** What this assignment is about. Absent = work without a resource (a rollup, a chore). */
|
|
63
54
|
subjectKind?: string;
|
|
64
55
|
subjectId?: string;
|
|
65
56
|
status: AssignmentStatus;
|
|
66
57
|
/**
|
|
67
|
-
*
|
|
58
|
+
* What the **assignment** is waiting on between two turns.
|
|
68
59
|
*
|
|
69
|
-
*
|
|
60
|
+
* Deliberately here and not on the run, because they are two different kinds of pause:
|
|
70
61
|
*
|
|
71
|
-
* -
|
|
72
|
-
*
|
|
73
|
-
* -
|
|
74
|
-
*
|
|
62
|
+
* - a **workflow** pauses *mid-flow* and resumes at step N+1 with the same vars — the cursor
|
|
63
|
+
* (`run.resumeAtStep`) is essential;
|
|
64
|
+
* - an **assignment** has no cursor. "Resuming" is *taking another turn*, with the thread as
|
|
65
|
+
* memory.
|
|
75
66
|
*
|
|
76
|
-
*
|
|
67
|
+
* Squeezing those two into one field is why every procedure run stored `resumeAtStep: 0`.
|
|
77
68
|
*/
|
|
78
69
|
waitingOn?: WaitingOn | null;
|
|
79
|
-
/**
|
|
70
|
+
/** The kind of `waitingOn` as a flat column — the store cannot search inside a nested object. */
|
|
80
71
|
waitingOnKind?: WaitingOn["on"] | null;
|
|
81
72
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* Was `run.rounds`, wat het aantal keer parkeren *binnen één run* telde. Op dit niveau is het de
|
|
85
|
-
* teller die telt: een agent die met een mens heen en weer blijft pingpongen moet een plafond
|
|
86
|
-
* hebben.
|
|
73
|
+
* How many turns this assignment has had — counted at assignment level, because an agent
|
|
74
|
+
* ping-ponging with a human needs a ceiling.
|
|
87
75
|
*/
|
|
88
76
|
turns: number;
|
|
89
|
-
/**
|
|
77
|
+
/** The thread: agent and human both write in it. This is the assignment's state. */
|
|
90
78
|
conversationId: string;
|
|
91
|
-
/**
|
|
79
|
+
/** List index, not authorization — see `PlaybookRun.visibleTo`. */
|
|
92
80
|
visibleTo?: string[];
|
|
93
|
-
/**
|
|
81
|
+
/** What this assignment is about, in human words. For the list. */
|
|
94
82
|
title?: string;
|
|
95
83
|
/**
|
|
96
|
-
*
|
|
84
|
+
* *Why* the assignment is in this state, when there is something to explain.
|
|
97
85
|
*
|
|
98
|
-
* `decideAssignmentState`
|
|
99
|
-
*
|
|
100
|
-
*
|
|
86
|
+
* `decideAssignmentState` already computed this and nothing stored it, so you saw `escalated`
|
|
87
|
+
* without an explanation — exactly the question ("why did it hand this over?") this design should
|
|
88
|
+
* be able to answer. The agent's own reason goes here, and its words in the thread.
|
|
101
89
|
*/
|
|
102
90
|
statusReason?: string;
|
|
103
91
|
createdBy: string;
|
|
104
92
|
lastActivityAt?: number;
|
|
105
93
|
}
|
|
106
94
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* **Vier regels, van hard naar zacht.**
|
|
95
|
+
* Does this activity wake an assignment that is waiting for an answer?
|
|
110
96
|
*
|
|
111
|
-
*
|
|
112
|
-
* antwoord is per definitie het antwoord waar de opdracht op wachtte.
|
|
113
|
-
* 2. **De agent zelf schreef dit**: nooit. Zonder deze poort is de vraag die de agent stelt het
|
|
114
|
-
* signaal waarop hij wakker wordt — een lus die pas bij het beurtplafond stopt.
|
|
115
|
-
* 3. **De agent is genoemd**: wekt, ongeacht waarop hij wacht. Een vermelding is de expliciete
|
|
116
|
-
* vraag die de engine zelf niet kan beantwoorden: *is dit aan míjn agent gericht?*
|
|
117
|
-
* 4. **Een interne notitie terwijl hij op een antwoord wacht** (`waitingOn === "reply"`): wekt.
|
|
97
|
+
* **Four rules, hard to soft.**
|
|
118
98
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
99
|
+
* 1. **The customer replies** (`inbound`): always wakes. A customer cannot @-mention anyone, and
|
|
100
|
+
* their reply is by definition the answer the assignment was waiting for.
|
|
101
|
+
* 2. **The agent wrote this itself**: never. Without this gate, the question the agent asks is the
|
|
102
|
+
* signal it wakes on — a loop that stops only at the turn ceiling.
|
|
103
|
+
* 3. **The agent is mentioned**: wakes, whatever it is waiting on. A mention is the explicit answer
|
|
104
|
+
* to the question the engine cannot answer itself: *is this addressed to my agent?*
|
|
105
|
+
* 4. **An internal note while it waits for a reply** (`waitingOn === "reply"`): wakes. If an agent
|
|
106
|
+
* explicitly asked for an answer, the next thing a colleague writes *is* that answer; forcing
|
|
107
|
+
* someone to @-mention their own agent on the question it just asked is a ritual, not a signal.
|
|
108
|
+
* The worry it replaces ("two colleagues conferring cost a turn each") stays covered by the
|
|
109
|
+
* edge: `waitingOn` has to be `"reply"`, and once it waits on a task or is still busy, a mention
|
|
110
|
+
* is again the only way in.
|
|
125
111
|
*
|
|
126
|
-
* @param waitingOn
|
|
127
|
-
*
|
|
112
|
+
* @param waitingOn What the assignment waits on. Omitted = rules 1-3 only, so a caller that does
|
|
113
|
+
* not know never wakes too eagerly.
|
|
128
114
|
*/
|
|
129
|
-
export declare function wakesAssignment(activity:
|
|
130
|
-
/**
|
|
115
|
+
export declare function wakesAssignment(activity: FeedActivity, agentId: string, waitingOn?: WaitingOn["on"]): boolean;
|
|
116
|
+
/** The subject of an assignment as one reference, or `undefined`. */
|
|
131
117
|
export declare function assignmentSubject(assignment: {
|
|
132
118
|
subjectKind?: string;
|
|
133
119
|
subjectId?: string;
|
|
134
120
|
}): ResourceRef | undefined;
|
|
135
121
|
/**
|
|
136
|
-
*
|
|
122
|
+
* How many turns an assignment may take before a human takes over.
|
|
137
123
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
124
|
+
* Not unlimited, because every turn is a model call and an agent can keep asking with the best
|
|
125
|
+
* intentions. At the ceiling it escalates, and that is visible in the thread — an assignment that
|
|
126
|
+
* silently stops would make "why is it doing nothing any more" unanswerable.
|
|
141
127
|
*/
|
|
142
128
|
export declare const MAX_ASSIGNMENT_TURNS = 12;
|
|
143
129
|
/**
|
|
144
|
-
*
|
|
130
|
+
* What a turn chose as its next move. This is what the control tools record.
|
|
145
131
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
132
|
+
* One union, because the outcomes are mutually exclusive: you wait for something, you are done, or
|
|
133
|
+
* you hand it to a human.
|
|
148
134
|
*/
|
|
149
135
|
export type AssignmentControl = {
|
|
150
136
|
kind: "wait";
|
|
@@ -157,15 +143,15 @@ export type AssignmentControl = {
|
|
|
157
143
|
reason: string;
|
|
158
144
|
};
|
|
159
145
|
/**
|
|
160
|
-
*
|
|
146
|
+
* The state the assignment is in after this turn.
|
|
161
147
|
*
|
|
162
|
-
*
|
|
163
|
-
* `decideFinalState`
|
|
148
|
+
* Pure, so the rule sits in one place and is testable without Bridge — the same shape as
|
|
149
|
+
* `decideFinalState` for a run.
|
|
164
150
|
*
|
|
165
|
-
* **
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
151
|
+
* **No control call means escalate**, not silently stay open. That is the rule `decideFinalState`
|
|
152
|
+
* already applies to a run waiting without a valid reason ("fail loudly rather than park
|
|
153
|
+
* silently"): an agent finishing its turn without saying what it wants leaves a human with an
|
|
154
|
+
* assignment nobody knows is still going anywhere.
|
|
169
155
|
*/
|
|
170
156
|
export declare function decideAssignmentState(control: AssignmentControl | undefined, turnsSoFar: number, maxTurns?: number): {
|
|
171
157
|
status: AssignmentStatus;
|
|
@@ -7,8 +7,8 @@ export interface ChannelSignature {
|
|
|
7
7
|
}
|
|
8
8
|
export type ChannelSignaturesMap = Partial<Record<CommunicationIntent | string, ChannelSignature>>;
|
|
9
9
|
/**
|
|
10
|
-
* Intents
|
|
11
|
-
*
|
|
10
|
+
* Intents that carry a signature. Only these appear in the signature editor and are given a
|
|
11
|
+
* delimiter by `applySignature`.
|
|
12
12
|
*/
|
|
13
13
|
export declare const SIGNATURE_INTENTS: Set<string>;
|
|
14
14
|
/**
|
|
@@ -21,12 +21,11 @@ export declare const SIGNATURE_INTENTS: Set<string>;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function resolveSignature(channel: Channel, intent: CommunicationIntent | string): ChannelSignature | null;
|
|
23
23
|
/**
|
|
24
|
-
* Append
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* - text -> dubbele newline (plain-text varianten / timeline)
|
|
24
|
+
* Append the signature for this channel + intent to an outgoing body. Returns the body unchanged
|
|
25
|
+
* when there is no (enabled, non-empty) signature. One place for the delimiter logic so every
|
|
26
|
+
* provider uses the same formatting instead of duplicating it per send path:
|
|
27
|
+
* - mail (html) -> RFC-style "-- " separator
|
|
28
|
+
* - message (html) -> double <br>
|
|
29
|
+
* - text -> double newline (plain-text variants / timeline)
|
|
31
30
|
*/
|
|
32
31
|
export declare function applySignature(channel: Channel | undefined, intent: CommunicationIntent | string, body: string, format?: "html" | "text"): string;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The kinds of custom field.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Started as `text | select`, because that was all a conversation needed. Work items ask for more:
|
|
5
|
+
* a deal value should show as an amount, a lead time as a duration, and a customer as a reference
|
|
6
|
+
* into the company register instead of as free text.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Deliberately **no** `multi_select`: {@link CustomFieldDef.multiValued} already exists, is
|
|
9
|
+
* already stored, and means exactly that. Two ways to say one thing yields a renderer with a
|
|
10
|
+
* branch nobody maintains.
|
|
11
11
|
*/
|
|
12
12
|
export type CustomFieldType = "text" | "textarea" | "number" | "currency" | "date" | "duration" | "select" | "user" | "checkbox"
|
|
13
|
-
/**
|
|
13
|
+
/** Points at a resource elsewhere in the platform; see {@link CustomFieldDef.refKinds}. */
|
|
14
14
|
| "resource_ref";
|
|
15
15
|
/** A selectable option for a `select` field. `label` falls back to `value` when absent. */
|
|
16
16
|
export interface CustomFieldOption {
|
|
@@ -39,17 +39,17 @@ export interface CustomFieldDef {
|
|
|
39
39
|
/** Empty/absent = all teams; otherwise only shown when the resource belongs to one of these teams. */
|
|
40
40
|
teamIds?: string[];
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* For `resource_ref`: which scope kinds may be chosen (`"company"`, `"contact"`, `"work_item"`).
|
|
43
|
+
* Empty = whatever the picker offers.
|
|
44
44
|
*/
|
|
45
45
|
refKinds?: string[];
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Which work projects switch this field on. Empty/absent = every project, as with
|
|
48
48
|
* {@link CustomFieldDef.teamIds}.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Exists because a field definition is platform-wide while a field set is not: two projects may
|
|
51
|
+
* both know an "Amount" with a meaning of their own. Additive, so existing definitions keep an
|
|
52
|
+
* empty list and stay valid everywhere.
|
|
53
53
|
*/
|
|
54
54
|
projectIds?: string[];
|
|
55
55
|
/**
|
|
@@ -1,107 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Who is "Asana user 1203847" here?
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Asana
|
|
6
|
-
* platform
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* This layer did not exist, which is exactly why `apps/asana` leaves its `assignees` empty: an
|
|
5
|
+
* Asana assignee is a gid, and copying it one-to-one would assign work to an id that is nobody in
|
|
6
|
+
* this platform — work in a ghost's name. The only two options were "leave the field out" or "a
|
|
7
|
+
* hack per app", and every next connector syncing assignees, authors, reviewers or commenters runs
|
|
8
|
+
* into it again.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* An external identity is **not** the same as the four things that resemble it:
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* | Lijkt erop | Is | Waarom niet hetzelfde |
|
|
12
|
+
* | Resembles it | Is | Why it differs |
|
|
15
13
|
* |---|---|---|
|
|
16
|
-
* | `externalId`
|
|
17
|
-
* | `ManagedAccount.metadata.externalUserId` |
|
|
18
|
-
* | `ManagedAccount.userId` |
|
|
19
|
-
* | `Contact` / shadow contact |
|
|
20
|
-
*
|
|
21
|
-
* De vraag hier is de vijfde: welke platform-gebruiker hóórt bij dit externe account. Die heeft
|
|
22
|
-
* een eigen levensduur (je koppelt hem één keer, hij geldt voor elke connector-ronde daarna) en
|
|
23
|
-
* een eigen beheer-oppervlak ("3 van de 12 Asana-gebruikers gekoppeld").
|
|
14
|
+
* | `externalId` on a record | dedupe axis for one row | says nothing about people |
|
|
15
|
+
* | `ManagedAccount.metadata.externalUserId` | the identity of a *connected account* | one per account, and only of whoever connected themselves |
|
|
16
|
+
* | `ManagedAccount.userId` | whose credential this is | ownership, not a mapping |
|
|
17
|
+
* | `Contact` / shadow contact | a person *outside* the organisation | an assignee is someone *inside* the organisation, in an external system |
|
|
24
18
|
*
|
|
25
|
-
*
|
|
19
|
+
* The question here is the fifth: which platform user *belongs* to this external account. It has a
|
|
20
|
+
* lifetime of its own (you link it once, it holds for every connector round after) and an admin
|
|
21
|
+
* surface of its own ("3 of the 12 Asana users linked").
|
|
26
22
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* It lives in `apps/user`, with the app that owns users: the email bootstrap needs the tenant
|
|
24
|
+
* roster, which is there, and non-sync consumers (AI tools, webhooks, companion apps) can use it
|
|
25
|
+
* just as well. Putting it in the sync pipeline would make it unusable outside sync.
|
|
30
26
|
*/
|
|
31
27
|
/**
|
|
32
|
-
*
|
|
28
|
+
* How sure are we this link is right?
|
|
33
29
|
*
|
|
34
|
-
*
|
|
35
|
-
* `email-match`
|
|
36
|
-
*
|
|
30
|
+
* The three values are not a quality label but a **decision axis**: only `confirmed` and
|
|
31
|
+
* `email-match` may carry a `userId`, and an `unmatched` row exists precisely to make visible that
|
|
32
|
+
* there is something to link.
|
|
37
33
|
*/
|
|
38
34
|
export type ExternalIdentityConfidence =
|
|
39
|
-
/**
|
|
35
|
+
/** A person linked this. Never overwritten by a bootstrap. */
|
|
40
36
|
"confirmed"
|
|
41
|
-
/**
|
|
37
|
+
/** The external address matched an active user's address exactly. */
|
|
42
38
|
| "email-match"
|
|
43
|
-
/**
|
|
39
|
+
/** Known external account, no platform user found. `userId` is empty. */
|
|
44
40
|
| "unmatched";
|
|
45
41
|
export interface ExternalIdentity {
|
|
46
42
|
id: string;
|
|
47
43
|
organizationId: string;
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
50
|
-
* (`"microsoft"`) —
|
|
45
|
+
* Which external system. The connector's catalog key (`"asana"`) or the app namespace
|
|
46
|
+
* (`"microsoft"`) — the same vocabulary as `SyncSourceDefinition.connectorHint`.
|
|
51
47
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
48
|
+
* Deliberately not the connector **id**: two organisation connections to the same product share
|
|
49
|
+
* the same users, and you would then have to do the mapping twice.
|
|
54
50
|
*/
|
|
55
51
|
connectorKey: string;
|
|
56
|
-
/**
|
|
52
|
+
/** This person's id in that system: an Asana gid, a Graph `oid`, a Slack user id. */
|
|
57
53
|
externalId: string;
|
|
58
|
-
/**
|
|
54
|
+
/** The platform user. Empty with `confidence: "unmatched"`. */
|
|
59
55
|
userId?: string;
|
|
60
|
-
/**
|
|
56
|
+
/** The address the external system gave. Kept so a later bootstrap can match. */
|
|
61
57
|
externalEmail?: string;
|
|
62
|
-
/**
|
|
58
|
+
/** The name the external system gave. Only to keep the admin list readable. */
|
|
63
59
|
externalName?: string;
|
|
64
60
|
confidence: ExternalIdentityConfidence;
|
|
65
61
|
createdAt?: number;
|
|
66
62
|
updatedAt?: number;
|
|
67
63
|
}
|
|
68
|
-
/**
|
|
64
|
+
/** One external account a connector is asking about. */
|
|
69
65
|
export interface ExternalIdentityQuery {
|
|
70
66
|
externalId: string;
|
|
71
|
-
/**
|
|
67
|
+
/** When the external system gives an address. Without it the bootstrap can match nothing. */
|
|
72
68
|
email?: string;
|
|
73
69
|
name?: string;
|
|
74
70
|
}
|
|
75
71
|
/**
|
|
76
|
-
* `POST /external-identity/resolve` — **
|
|
72
|
+
* `POST /external-identity/resolve` — **batched**, because one sync page carries dozens.
|
|
77
73
|
*
|
|
78
|
-
*
|
|
79
|
-
* `landRecords`
|
|
74
|
+
* Asking per record would cost dozens of app boundary crossings per page, exactly the mistake
|
|
75
|
+
* `landRecords` already avoids by grouping per kind.
|
|
80
76
|
*/
|
|
81
77
|
export interface ResolveExternalIdentitiesRequest {
|
|
82
78
|
connectorKey: string;
|
|
83
79
|
externals: ExternalIdentityQuery[];
|
|
84
80
|
}
|
|
85
|
-
/**
|
|
81
|
+
/** What the resolve returns per external id. `userId` absent = nothing to assign to. */
|
|
86
82
|
export interface ResolvedExternalIdentity {
|
|
87
83
|
userId?: string;
|
|
88
84
|
confidence: ExternalIdentityConfidence;
|
|
89
85
|
}
|
|
90
86
|
export interface ResolveExternalIdentitiesResponse {
|
|
91
87
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
88
|
+
* Keyed on `externalId`. Every requested id is in there, the unmatched ones included — a missing
|
|
89
|
+
* key and "no match" are two different things, and the caller must not have to guess.
|
|
94
90
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* heel aan.
|
|
91
|
+
* A plain map is safe here: keys travel intact, so a gid with a dot or a dash in it arrives
|
|
92
|
+
* whole.
|
|
98
93
|
*/
|
|
99
94
|
resolved: Record<string, ResolvedExternalIdentity>;
|
|
100
95
|
}
|
|
101
|
-
/** `POST /external-identity/link` —
|
|
96
|
+
/** `POST /external-identity/link` — a person points out the link. */
|
|
102
97
|
export interface LinkExternalIdentityRequest {
|
|
103
98
|
connectorKey: string;
|
|
104
99
|
externalId: string;
|
|
105
|
-
/**
|
|
100
|
+
/** Empty = unlink; the row stays as `unmatched`. */
|
|
106
101
|
userId?: string;
|
|
107
102
|
}
|