@opengeni/core 0.21.10 → 0.28.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/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/canonical-human-identities.d.ts +12 -0
- package/dist/canonical-human-identities.js +23 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-IBOEYG6N.js +34 -0
- package/dist/chunk-IBOEYG6N.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +30 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/conversation-integrations.d.ts +225 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/fiken.d.ts +35 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +24 -8
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +10 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +37 -26
- package/dist/index.js +4145 -959
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +6 -2
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/fleet.d.ts +8 -5
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +4 -2
- package/dist/transcription.d.ts +5 -0
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +22 -10
- package/src/access/index.ts +14 -2
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/canonical-human-identities.ts +27 -0
- package/src/dependencies.ts +34 -2
- package/src/domain/capabilities.ts +729 -408
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/conversation-integrations.ts +1238 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/fiken.ts +88 -0
- package/src/domain/insights.ts +163 -50
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +171 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +395 -148
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +104 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +15 -0
- package/src/managed-session.ts +19 -2
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/fleet.ts +72 -66
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +187 -52
- package/src/transcription.ts +5 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
declare const conversationProviderNamespaceBrand: unique symbol;
|
|
4
|
+
declare const conversationProviderOpaqueValueBrand: unique symbol;
|
|
5
|
+
declare const conversationEventIdentityBrand: unique symbol;
|
|
6
|
+
declare const conversationRouteIdentityBrand: unique symbol;
|
|
7
|
+
declare const conversationActorIdentityBrand: unique symbol;
|
|
8
|
+
declare const conversationOperationIdentityBrand: unique symbol;
|
|
9
|
+
declare const conversationRequestDigestBrand: unique symbol;
|
|
10
|
+
|
|
11
|
+
export const CONVERSATION_INTEGRATION_SCHEMA_VERSION = 1 as const;
|
|
12
|
+
export const CONVERSATION_PROVIDER_NAMESPACE_MAX_UTF8_BYTES = 128;
|
|
13
|
+
export const CONVERSATION_PROVIDER_OPAQUE_ID_MAX_UTF8_BYTES = 512;
|
|
14
|
+
export const CONVERSATION_TEXT_MAX_UTF8_BYTES = 32 * 1024;
|
|
15
|
+
export const CONVERSATION_ATTACHMENT_MAX_COUNT = 16;
|
|
16
|
+
export const CONVERSATION_ATTACHMENT_NAME_MAX_UTF8_BYTES = 1024;
|
|
17
|
+
export const CONVERSATION_ATTACHMENT_MEDIA_TYPE_MAX_UTF8_BYTES = 128;
|
|
18
|
+
export const CONVERSATION_OPERATION_KEY_MAX_UTF8_BYTES = 512;
|
|
19
|
+
export const CONVERSATION_OUTCOME_CODE_MAX_UTF8_BYTES = 128;
|
|
20
|
+
|
|
21
|
+
const PROVIDER_NAMESPACE_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u;
|
|
22
|
+
const OUTCOME_CODE_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;
|
|
23
|
+
const MEDIA_TYPE_PATTERN = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/iu;
|
|
24
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
25
|
+
const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//iu;
|
|
26
|
+
const textEncoder = new TextEncoder();
|
|
27
|
+
|
|
28
|
+
export type ConversationProviderNamespace = string & {
|
|
29
|
+
readonly [conversationProviderNamespaceBrand]: true;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ConversationProviderOpaqueValue = string & {
|
|
33
|
+
readonly [conversationProviderOpaqueValueBrand]: true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ConversationProviderOpaqueIdKind =
|
|
37
|
+
| "installation"
|
|
38
|
+
| "actor"
|
|
39
|
+
| "conversation"
|
|
40
|
+
| "thread"
|
|
41
|
+
| "event"
|
|
42
|
+
| "message"
|
|
43
|
+
| "attachment"
|
|
44
|
+
| "receipt";
|
|
45
|
+
|
|
46
|
+
export type ConversationProviderOpaqueId<
|
|
47
|
+
TKind extends ConversationProviderOpaqueIdKind = ConversationProviderOpaqueIdKind,
|
|
48
|
+
> = Readonly<{
|
|
49
|
+
provider: ConversationProviderNamespace;
|
|
50
|
+
kind: TKind;
|
|
51
|
+
value: ConversationProviderOpaqueValue;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
export type ConversationInstallationIdentity = ConversationProviderOpaqueId<"installation">;
|
|
55
|
+
export type ConversationEventIdentity = `ciev1_${string}` & {
|
|
56
|
+
readonly [conversationEventIdentityBrand]: true;
|
|
57
|
+
};
|
|
58
|
+
export type ConversationRouteIdentity = `cirt1_${string}` & {
|
|
59
|
+
readonly [conversationRouteIdentityBrand]: true;
|
|
60
|
+
};
|
|
61
|
+
export type ConversationActorIdentity = `ciac1_${string}` & {
|
|
62
|
+
readonly [conversationActorIdentityBrand]: true;
|
|
63
|
+
};
|
|
64
|
+
export type ConversationOperationIdentity = `ciop1_${string}` & {
|
|
65
|
+
readonly [conversationOperationIdentityBrand]: true;
|
|
66
|
+
};
|
|
67
|
+
export type ConversationRequestDigest = `sha256:${string}` & {
|
|
68
|
+
readonly [conversationRequestDigestBrand]: true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ConversationActorKind = "human" | "bot" | "system";
|
|
72
|
+
|
|
73
|
+
export type ConversationActorInput = Readonly<{
|
|
74
|
+
providerActorId: string;
|
|
75
|
+
kind: ConversationActorKind;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
export type ConversationActor = Readonly<{
|
|
79
|
+
installation: ConversationInstallationIdentity;
|
|
80
|
+
providerActorId: ConversationProviderOpaqueId<"actor">;
|
|
81
|
+
kind: ConversationActorKind;
|
|
82
|
+
identity: ConversationActorIdentity;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
export type ConversationRoute = Readonly<{
|
|
86
|
+
installation: ConversationInstallationIdentity;
|
|
87
|
+
providerConversationId: ConversationProviderOpaqueId<"conversation">;
|
|
88
|
+
providerThreadId: ConversationProviderOpaqueId<"thread"> | null;
|
|
89
|
+
identity: ConversationRouteIdentity;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
export type ConversationRouteInput = Readonly<{
|
|
93
|
+
providerConversationId: string;
|
|
94
|
+
providerThreadId?: string | null;
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
export type ConversationStartSignal = Readonly<{ kind: "start" }>;
|
|
98
|
+
export type ConversationContinueSignal = Readonly<{ kind: "continue" }>;
|
|
99
|
+
export type ConversationControlSignal = Readonly<{
|
|
100
|
+
kind: "control";
|
|
101
|
+
control: "stop" | "resume";
|
|
102
|
+
}>;
|
|
103
|
+
export type ConversationSignal =
|
|
104
|
+
| ConversationStartSignal
|
|
105
|
+
| ConversationContinueSignal
|
|
106
|
+
| ConversationControlSignal;
|
|
107
|
+
|
|
108
|
+
export type ConversationAttachmentReference = Readonly<{
|
|
109
|
+
providerAttachmentId: ConversationProviderOpaqueId<"attachment">;
|
|
110
|
+
fileName: string | null;
|
|
111
|
+
mediaType: string | null;
|
|
112
|
+
byteSize: number | null;
|
|
113
|
+
contentSha256: string | null;
|
|
114
|
+
}>;
|
|
115
|
+
|
|
116
|
+
export type ConversationAttachmentReferenceInput = Readonly<{
|
|
117
|
+
providerAttachmentId: string;
|
|
118
|
+
fileName?: string | null;
|
|
119
|
+
mediaType?: string | null;
|
|
120
|
+
byteSize?: number | null;
|
|
121
|
+
contentSha256?: string | null;
|
|
122
|
+
}>;
|
|
123
|
+
|
|
124
|
+
export type ConversationInboundEnvelope = Readonly<{
|
|
125
|
+
schemaVersion: typeof CONVERSATION_INTEGRATION_SCHEMA_VERSION;
|
|
126
|
+
installation: ConversationInstallationIdentity;
|
|
127
|
+
actor: ConversationActor;
|
|
128
|
+
route: ConversationRoute;
|
|
129
|
+
providerEventId: ConversationProviderOpaqueId<"event">;
|
|
130
|
+
providerMessageId: ConversationProviderOpaqueId<"message">;
|
|
131
|
+
eventIdentity: ConversationEventIdentity;
|
|
132
|
+
occurredAt: string;
|
|
133
|
+
signal: ConversationSignal;
|
|
134
|
+
text: string;
|
|
135
|
+
attachments: readonly ConversationAttachmentReference[];
|
|
136
|
+
}>;
|
|
137
|
+
|
|
138
|
+
export type ConversationInboundEnvelopeInput = Readonly<{
|
|
139
|
+
provider: string;
|
|
140
|
+
installationId: string;
|
|
141
|
+
actor: ConversationActorInput;
|
|
142
|
+
route: ConversationRouteInput;
|
|
143
|
+
providerEventId: string;
|
|
144
|
+
providerMessageId: string;
|
|
145
|
+
occurredAt: string;
|
|
146
|
+
signal: ConversationSignal;
|
|
147
|
+
text: string;
|
|
148
|
+
attachments?: readonly ConversationAttachmentReferenceInput[];
|
|
149
|
+
}>;
|
|
150
|
+
|
|
151
|
+
type ConversationDeliveryCommandBase = Readonly<{
|
|
152
|
+
schemaVersion: typeof CONVERSATION_INTEGRATION_SCHEMA_VERSION;
|
|
153
|
+
operationId: ConversationOperationIdentity;
|
|
154
|
+
logicalOperationKey: string;
|
|
155
|
+
requestDigest: ConversationRequestDigest;
|
|
156
|
+
installation: ConversationInstallationIdentity;
|
|
157
|
+
route: ConversationRoute;
|
|
158
|
+
}>;
|
|
159
|
+
|
|
160
|
+
export type ConversationPostCommand = ConversationDeliveryCommandBase &
|
|
161
|
+
Readonly<{
|
|
162
|
+
kind: "post";
|
|
163
|
+
targetProviderMessageId: null;
|
|
164
|
+
text: string;
|
|
165
|
+
}>;
|
|
166
|
+
|
|
167
|
+
export type ConversationUpdateCommand = ConversationDeliveryCommandBase &
|
|
168
|
+
Readonly<{
|
|
169
|
+
kind: "update";
|
|
170
|
+
targetProviderMessageId: ConversationProviderOpaqueId<"message">;
|
|
171
|
+
text: string;
|
|
172
|
+
}>;
|
|
173
|
+
|
|
174
|
+
export type ConversationDeleteCommand = ConversationDeliveryCommandBase &
|
|
175
|
+
Readonly<{
|
|
176
|
+
kind: "delete";
|
|
177
|
+
targetProviderMessageId: ConversationProviderOpaqueId<"message">;
|
|
178
|
+
text: null;
|
|
179
|
+
}>;
|
|
180
|
+
|
|
181
|
+
export type ConversationDeliveryCommand =
|
|
182
|
+
| ConversationPostCommand
|
|
183
|
+
| ConversationUpdateCommand
|
|
184
|
+
| ConversationDeleteCommand;
|
|
185
|
+
|
|
186
|
+
export type ConversationDeliveryCommandInput =
|
|
187
|
+
| Readonly<{
|
|
188
|
+
kind: "post";
|
|
189
|
+
logicalOperationKey: string;
|
|
190
|
+
installation: ConversationInstallationIdentity;
|
|
191
|
+
route: ConversationRoute;
|
|
192
|
+
text: string;
|
|
193
|
+
}>
|
|
194
|
+
| Readonly<{
|
|
195
|
+
kind: "update";
|
|
196
|
+
logicalOperationKey: string;
|
|
197
|
+
installation: ConversationInstallationIdentity;
|
|
198
|
+
route: ConversationRoute;
|
|
199
|
+
targetProviderMessageId: string;
|
|
200
|
+
text: string;
|
|
201
|
+
}>
|
|
202
|
+
| Readonly<{
|
|
203
|
+
kind: "delete";
|
|
204
|
+
logicalOperationKey: string;
|
|
205
|
+
installation: ConversationInstallationIdentity;
|
|
206
|
+
route: ConversationRoute;
|
|
207
|
+
targetProviderMessageId: string;
|
|
208
|
+
}>;
|
|
209
|
+
|
|
210
|
+
export type ConversationProviderReceipt = Readonly<{
|
|
211
|
+
schemaVersion: typeof CONVERSATION_INTEGRATION_SCHEMA_VERSION;
|
|
212
|
+
operationId: ConversationOperationIdentity;
|
|
213
|
+
requestDigest: ConversationRequestDigest;
|
|
214
|
+
installation: ConversationInstallationIdentity;
|
|
215
|
+
providerMessageId: ConversationProviderOpaqueId<"message">;
|
|
216
|
+
providerReceiptId: ConversationProviderOpaqueId<"receipt"> | null;
|
|
217
|
+
observedAt: string;
|
|
218
|
+
}>;
|
|
219
|
+
|
|
220
|
+
export type ConversationProviderReceiptInput = Readonly<{
|
|
221
|
+
command: ConversationDeliveryCommand;
|
|
222
|
+
providerMessageId: string;
|
|
223
|
+
providerReceiptId?: string | null;
|
|
224
|
+
observedAt: string;
|
|
225
|
+
}>;
|
|
226
|
+
|
|
227
|
+
export type ConversationDeliveryNotStarted = Readonly<{
|
|
228
|
+
status: "not_started";
|
|
229
|
+
code: string;
|
|
230
|
+
}>;
|
|
231
|
+
export type ConversationDeliveryUnknown = Readonly<{
|
|
232
|
+
status: "unknown";
|
|
233
|
+
code: string;
|
|
234
|
+
}>;
|
|
235
|
+
export type ConversationDeliveryRetryableFailure = Readonly<{
|
|
236
|
+
status: "retryable_failure";
|
|
237
|
+
code: string;
|
|
238
|
+
retryAfterMs: number | null;
|
|
239
|
+
}>;
|
|
240
|
+
export type ConversationDeliveryPermanentFailure = Readonly<{
|
|
241
|
+
status: "permanent_failure";
|
|
242
|
+
code: string;
|
|
243
|
+
}>;
|
|
244
|
+
export type ConversationDeliverySuccess = Readonly<{
|
|
245
|
+
status: "success";
|
|
246
|
+
receipt: ConversationProviderReceipt;
|
|
247
|
+
}>;
|
|
248
|
+
|
|
249
|
+
export type ConversationDeliveryOutcome =
|
|
250
|
+
| ConversationDeliveryNotStarted
|
|
251
|
+
| ConversationDeliveryUnknown
|
|
252
|
+
| ConversationDeliveryRetryableFailure
|
|
253
|
+
| ConversationDeliveryPermanentFailure
|
|
254
|
+
| ConversationDeliverySuccess;
|
|
255
|
+
|
|
256
|
+
export type ConversationDeliveryOutcomeInput =
|
|
257
|
+
| ConversationDeliveryNotStarted
|
|
258
|
+
| ConversationDeliveryUnknown
|
|
259
|
+
| Readonly<{
|
|
260
|
+
status: "retryable_failure";
|
|
261
|
+
code: string;
|
|
262
|
+
retryAfterMs?: number | null;
|
|
263
|
+
}>
|
|
264
|
+
| ConversationDeliveryPermanentFailure
|
|
265
|
+
| ConversationDeliverySuccess;
|
|
266
|
+
|
|
267
|
+
export type ConversationDeliveryNextAction =
|
|
268
|
+
| "retry_same_operation"
|
|
269
|
+
| "reconcile"
|
|
270
|
+
| "stop"
|
|
271
|
+
| "complete";
|
|
272
|
+
|
|
273
|
+
export function normalizeConversationInboundEnvelope(
|
|
274
|
+
input: ConversationInboundEnvelopeInput,
|
|
275
|
+
): ConversationInboundEnvelope {
|
|
276
|
+
assertExactObject(input, "inbound envelope", [
|
|
277
|
+
"provider",
|
|
278
|
+
"installationId",
|
|
279
|
+
"actor",
|
|
280
|
+
"route",
|
|
281
|
+
"providerEventId",
|
|
282
|
+
"providerMessageId",
|
|
283
|
+
"occurredAt",
|
|
284
|
+
"signal",
|
|
285
|
+
"text",
|
|
286
|
+
"attachments",
|
|
287
|
+
]);
|
|
288
|
+
const provider = normalizeConversationProviderNamespace(input.provider);
|
|
289
|
+
const installation = normalizeConversationInstallationIdentity({
|
|
290
|
+
provider,
|
|
291
|
+
providerInstallationId: input.installationId,
|
|
292
|
+
});
|
|
293
|
+
const actor = normalizeConversationActor(installation, input.actor);
|
|
294
|
+
const route = normalizeConversationRoute(installation, input.route);
|
|
295
|
+
const providerEventId = providerOpaqueId(provider, "event", input.providerEventId);
|
|
296
|
+
const providerMessageId = providerOpaqueId(provider, "message", input.providerMessageId);
|
|
297
|
+
const occurredAt = canonicalTimestamp(input.occurredAt, "inbound occurredAt");
|
|
298
|
+
const signal = normalizeSignal(input.signal);
|
|
299
|
+
const text = boundedExactText(input.text, "inbound text", true);
|
|
300
|
+
const rawAttachments = input.attachments ?? [];
|
|
301
|
+
if (!Array.isArray(rawAttachments)) throw new TypeError("inbound attachments must be an array");
|
|
302
|
+
if (rawAttachments.length > CONVERSATION_ATTACHMENT_MAX_COUNT) {
|
|
303
|
+
throw new RangeError(
|
|
304
|
+
`inbound attachments must contain at most ${CONVERSATION_ATTACHMENT_MAX_COUNT} entries`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const attachments = Object.freeze(
|
|
308
|
+
rawAttachments.map((attachment) => normalizeAttachment(provider, attachment)),
|
|
309
|
+
);
|
|
310
|
+
if ((signal.kind === "start" || signal.kind === "continue") && !text && !attachments.length) {
|
|
311
|
+
throw new TypeError("start and continue signals require text or an attachment reference");
|
|
312
|
+
}
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
schemaVersion: CONVERSATION_INTEGRATION_SCHEMA_VERSION,
|
|
315
|
+
installation,
|
|
316
|
+
actor,
|
|
317
|
+
route,
|
|
318
|
+
providerEventId,
|
|
319
|
+
providerMessageId,
|
|
320
|
+
eventIdentity: deriveConversationEventIdentity(installation, providerEventId),
|
|
321
|
+
occurredAt,
|
|
322
|
+
signal,
|
|
323
|
+
text,
|
|
324
|
+
attachments,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function normalizeConversationProviderNamespace(
|
|
329
|
+
value: string,
|
|
330
|
+
): ConversationProviderNamespace {
|
|
331
|
+
return providerNamespace(value);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function normalizeConversationInstallationIdentity(input: {
|
|
335
|
+
provider: string | ConversationProviderNamespace;
|
|
336
|
+
providerInstallationId: string;
|
|
337
|
+
}): ConversationInstallationIdentity {
|
|
338
|
+
assertExactObject(input, "installation identity", ["provider", "providerInstallationId"]);
|
|
339
|
+
return providerOpaqueId(
|
|
340
|
+
providerNamespace(input.provider),
|
|
341
|
+
"installation",
|
|
342
|
+
input.providerInstallationId,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function normalizeConversationActor(
|
|
347
|
+
installation: ConversationInstallationIdentity,
|
|
348
|
+
input: ConversationActorInput,
|
|
349
|
+
): ConversationActor {
|
|
350
|
+
assertConversationInstallationIdentity(installation);
|
|
351
|
+
assertExactObject(input, "inbound actor", ["providerActorId", "kind"]);
|
|
352
|
+
if (input.kind !== "human" && input.kind !== "bot" && input.kind !== "system") {
|
|
353
|
+
throw new TypeError("inbound actor kind is unsupported");
|
|
354
|
+
}
|
|
355
|
+
const providerActorId = providerOpaqueId(installation.provider, "actor", input.providerActorId);
|
|
356
|
+
return Object.freeze({
|
|
357
|
+
installation,
|
|
358
|
+
providerActorId,
|
|
359
|
+
kind: input.kind,
|
|
360
|
+
identity: deriveConversationActorIdentity(installation, providerActorId),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function normalizeConversationRoute(
|
|
365
|
+
installation: ConversationInstallationIdentity,
|
|
366
|
+
input: ConversationRouteInput,
|
|
367
|
+
): ConversationRoute {
|
|
368
|
+
assertConversationInstallationIdentity(installation);
|
|
369
|
+
assertExactObject(input, "inbound route", ["providerConversationId", "providerThreadId"]);
|
|
370
|
+
const providerConversationId = providerOpaqueId(
|
|
371
|
+
installation.provider,
|
|
372
|
+
"conversation",
|
|
373
|
+
input.providerConversationId,
|
|
374
|
+
);
|
|
375
|
+
const providerThreadId =
|
|
376
|
+
input.providerThreadId === undefined || input.providerThreadId === null
|
|
377
|
+
? null
|
|
378
|
+
: providerOpaqueId(installation.provider, "thread", input.providerThreadId);
|
|
379
|
+
const identity = deriveConversationRouteIdentity(
|
|
380
|
+
installation,
|
|
381
|
+
providerConversationId,
|
|
382
|
+
providerThreadId,
|
|
383
|
+
);
|
|
384
|
+
return Object.freeze({ installation, providerConversationId, providerThreadId, identity });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function normalizeConversationDeliveryCommand(
|
|
388
|
+
input: ConversationDeliveryCommandInput,
|
|
389
|
+
): ConversationDeliveryCommand {
|
|
390
|
+
const keys =
|
|
391
|
+
input.kind === "post"
|
|
392
|
+
? ["kind", "logicalOperationKey", "installation", "route", "text"]
|
|
393
|
+
: input.kind === "update"
|
|
394
|
+
? [
|
|
395
|
+
"kind",
|
|
396
|
+
"logicalOperationKey",
|
|
397
|
+
"installation",
|
|
398
|
+
"route",
|
|
399
|
+
"targetProviderMessageId",
|
|
400
|
+
"text",
|
|
401
|
+
]
|
|
402
|
+
: ["kind", "logicalOperationKey", "installation", "route", "targetProviderMessageId"];
|
|
403
|
+
assertExactObject(input, "delivery command", keys);
|
|
404
|
+
if (input.kind !== "post" && input.kind !== "update" && input.kind !== "delete") {
|
|
405
|
+
throw new TypeError("delivery command kind is unsupported");
|
|
406
|
+
}
|
|
407
|
+
assertConversationInstallationIdentity(input.installation);
|
|
408
|
+
assertConversationRoute(input.route);
|
|
409
|
+
assertSameInstallation(input.installation, input.route.installation, "delivery route");
|
|
410
|
+
const logicalOperationKey = boundedOpaqueString(
|
|
411
|
+
input.logicalOperationKey,
|
|
412
|
+
"logical operation key",
|
|
413
|
+
CONVERSATION_OPERATION_KEY_MAX_UTF8_BYTES,
|
|
414
|
+
);
|
|
415
|
+
const operationId = deriveConversationOperationIdentity(input.installation, logicalOperationKey);
|
|
416
|
+
const targetProviderMessageId =
|
|
417
|
+
input.kind === "post"
|
|
418
|
+
? null
|
|
419
|
+
: providerOpaqueId(input.installation.provider, "message", input.targetProviderMessageId);
|
|
420
|
+
const text =
|
|
421
|
+
input.kind === "delete" ? null : boundedExactText(input.text, "delivery text", false);
|
|
422
|
+
const requestDigest = deliveryRequestDigest({
|
|
423
|
+
kind: input.kind,
|
|
424
|
+
installation: input.installation,
|
|
425
|
+
route: input.route,
|
|
426
|
+
targetProviderMessageId,
|
|
427
|
+
text,
|
|
428
|
+
});
|
|
429
|
+
return Object.freeze({
|
|
430
|
+
schemaVersion: CONVERSATION_INTEGRATION_SCHEMA_VERSION,
|
|
431
|
+
operationId,
|
|
432
|
+
logicalOperationKey,
|
|
433
|
+
requestDigest,
|
|
434
|
+
installation: input.installation,
|
|
435
|
+
route: input.route,
|
|
436
|
+
kind: input.kind,
|
|
437
|
+
targetProviderMessageId,
|
|
438
|
+
text,
|
|
439
|
+
}) as ConversationDeliveryCommand;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function normalizeConversationProviderReceipt(
|
|
443
|
+
input: ConversationProviderReceiptInput,
|
|
444
|
+
): ConversationProviderReceipt {
|
|
445
|
+
assertExactObject(input, "provider receipt", [
|
|
446
|
+
"command",
|
|
447
|
+
"providerMessageId",
|
|
448
|
+
"providerReceiptId",
|
|
449
|
+
"observedAt",
|
|
450
|
+
]);
|
|
451
|
+
assertConversationDeliveryCommand(input.command);
|
|
452
|
+
const providerMessageId = providerOpaqueId(
|
|
453
|
+
input.command.installation.provider,
|
|
454
|
+
"message",
|
|
455
|
+
input.providerMessageId,
|
|
456
|
+
);
|
|
457
|
+
if (
|
|
458
|
+
input.command.targetProviderMessageId &&
|
|
459
|
+
providerMessageId.value !== input.command.targetProviderMessageId.value
|
|
460
|
+
) {
|
|
461
|
+
throw new TypeError("provider receipt message does not match the mutation target");
|
|
462
|
+
}
|
|
463
|
+
const providerReceiptId =
|
|
464
|
+
input.providerReceiptId === undefined || input.providerReceiptId === null
|
|
465
|
+
? null
|
|
466
|
+
: providerOpaqueId(input.command.installation.provider, "receipt", input.providerReceiptId);
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
schemaVersion: CONVERSATION_INTEGRATION_SCHEMA_VERSION,
|
|
469
|
+
operationId: input.command.operationId,
|
|
470
|
+
requestDigest: input.command.requestDigest,
|
|
471
|
+
installation: input.command.installation,
|
|
472
|
+
providerMessageId,
|
|
473
|
+
providerReceiptId,
|
|
474
|
+
observedAt: canonicalTimestamp(input.observedAt, "provider receipt observedAt"),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function normalizeConversationDeliveryOutcome(
|
|
479
|
+
input: ConversationDeliveryOutcomeInput,
|
|
480
|
+
): ConversationDeliveryOutcome {
|
|
481
|
+
if (input.status === "success") {
|
|
482
|
+
assertExactObject(input, "delivery success", ["status", "receipt"]);
|
|
483
|
+
assertConversationProviderReceipt(input.receipt);
|
|
484
|
+
return Object.freeze({ status: "success", receipt: input.receipt });
|
|
485
|
+
}
|
|
486
|
+
if (input.status === "retryable_failure") {
|
|
487
|
+
assertExactObject(input, "retryable delivery failure", ["status", "code", "retryAfterMs"]);
|
|
488
|
+
const retryAfterMs = input.retryAfterMs ?? null;
|
|
489
|
+
if (retryAfterMs !== null && (!Number.isSafeInteger(retryAfterMs) || retryAfterMs < 0)) {
|
|
490
|
+
throw new RangeError("retryAfterMs must be a non-negative safe integer or null");
|
|
491
|
+
}
|
|
492
|
+
return Object.freeze({
|
|
493
|
+
status: "retryable_failure",
|
|
494
|
+
code: outcomeCode(input.code),
|
|
495
|
+
retryAfterMs,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (
|
|
499
|
+
input.status !== "not_started" &&
|
|
500
|
+
input.status !== "unknown" &&
|
|
501
|
+
input.status !== "permanent_failure"
|
|
502
|
+
) {
|
|
503
|
+
throw new TypeError("delivery outcome status is unsupported");
|
|
504
|
+
}
|
|
505
|
+
assertExactObject(input, "delivery outcome", ["status", "code"]);
|
|
506
|
+
return Object.freeze({ status: input.status, code: outcomeCode(input.code) });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function conversationDeliveryNextAction(
|
|
510
|
+
outcome: ConversationDeliveryOutcome,
|
|
511
|
+
): ConversationDeliveryNextAction {
|
|
512
|
+
assertConversationDeliveryOutcome(outcome);
|
|
513
|
+
if (outcome.status === "unknown") return "reconcile";
|
|
514
|
+
if (outcome.status === "not_started" || outcome.status === "retryable_failure") {
|
|
515
|
+
return "retry_same_operation";
|
|
516
|
+
}
|
|
517
|
+
return outcome.status === "success" ? "complete" : "stop";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function assertConversationDeliveryMayRetry(
|
|
521
|
+
outcome: ConversationDeliveryOutcome,
|
|
522
|
+
): asserts outcome is ConversationDeliveryNotStarted | ConversationDeliveryRetryableFailure {
|
|
523
|
+
const action = conversationDeliveryNextAction(outcome);
|
|
524
|
+
if (action === "retry_same_operation") return;
|
|
525
|
+
if (action === "reconcile") {
|
|
526
|
+
throw new Error("unknown delivery outcome must be reconciled before retry");
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`delivery outcome ${outcome.status} may not be retried`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function assertConversationDeliveryCommandMatches(
|
|
532
|
+
expected: ConversationDeliveryCommand,
|
|
533
|
+
candidate: ConversationDeliveryCommand,
|
|
534
|
+
): void {
|
|
535
|
+
assertConversationDeliveryCommand(expected);
|
|
536
|
+
assertConversationDeliveryCommand(candidate);
|
|
537
|
+
if (expected.operationId !== candidate.operationId) {
|
|
538
|
+
throw new Error("delivery operation identity does not match");
|
|
539
|
+
}
|
|
540
|
+
if (expected.requestDigest !== candidate.requestDigest) {
|
|
541
|
+
throw new Error("delivery operation identity was reused for different request content");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function assertConversationInboundEnvelope(
|
|
546
|
+
value: unknown,
|
|
547
|
+
): asserts value is ConversationInboundEnvelope {
|
|
548
|
+
assertExactObject(value, "normalized inbound envelope", [
|
|
549
|
+
"schemaVersion",
|
|
550
|
+
"installation",
|
|
551
|
+
"actor",
|
|
552
|
+
"route",
|
|
553
|
+
"providerEventId",
|
|
554
|
+
"providerMessageId",
|
|
555
|
+
"eventIdentity",
|
|
556
|
+
"occurredAt",
|
|
557
|
+
"signal",
|
|
558
|
+
"text",
|
|
559
|
+
"attachments",
|
|
560
|
+
]);
|
|
561
|
+
if (value.schemaVersion !== CONVERSATION_INTEGRATION_SCHEMA_VERSION) {
|
|
562
|
+
throw new TypeError("inbound envelope schema version is unsupported");
|
|
563
|
+
}
|
|
564
|
+
assertConversationInstallationIdentity(value.installation);
|
|
565
|
+
assertConversationActor(value.actor);
|
|
566
|
+
assertConversationRoute(value.route);
|
|
567
|
+
assertProviderOpaqueId(value.providerEventId, "event");
|
|
568
|
+
assertProviderOpaqueId(value.providerMessageId, "message");
|
|
569
|
+
assertSameInstallation(value.installation, value.actor.installation, "inbound actor");
|
|
570
|
+
assertSameInstallation(value.installation, value.route.installation, "inbound route");
|
|
571
|
+
assertSameProvider(value.installation.provider, value.providerEventId.provider, "event id");
|
|
572
|
+
assertSameProvider(value.installation.provider, value.providerMessageId.provider, "message id");
|
|
573
|
+
const expectedEventIdentity = deriveConversationEventIdentity(
|
|
574
|
+
value.installation,
|
|
575
|
+
value.providerEventId,
|
|
576
|
+
);
|
|
577
|
+
if (value.eventIdentity !== expectedEventIdentity) {
|
|
578
|
+
throw new TypeError("inbound event identity is not canonical");
|
|
579
|
+
}
|
|
580
|
+
canonicalTimestamp(value.occurredAt, "inbound occurredAt");
|
|
581
|
+
const signal = normalizeSignal(value.signal);
|
|
582
|
+
const text = boundedExactText(value.text, "inbound text", true);
|
|
583
|
+
if (!Array.isArray(value.attachments))
|
|
584
|
+
throw new TypeError("inbound attachments must be an array");
|
|
585
|
+
if (value.attachments.length > CONVERSATION_ATTACHMENT_MAX_COUNT) {
|
|
586
|
+
throw new RangeError("inbound attachment count exceeds the contract bound");
|
|
587
|
+
}
|
|
588
|
+
for (const attachment of value.attachments) {
|
|
589
|
+
assertConversationAttachmentReference(attachment);
|
|
590
|
+
assertSameProvider(
|
|
591
|
+
value.installation.provider,
|
|
592
|
+
attachment.providerAttachmentId.provider,
|
|
593
|
+
"attachment id",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
(signal.kind === "start" || signal.kind === "continue") &&
|
|
598
|
+
!text &&
|
|
599
|
+
!value.attachments.length
|
|
600
|
+
) {
|
|
601
|
+
throw new TypeError("start and continue signals require text or an attachment reference");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function assertConversationDeliveryCommand(
|
|
606
|
+
value: unknown,
|
|
607
|
+
): asserts value is ConversationDeliveryCommand {
|
|
608
|
+
assertExactObject(value, "normalized delivery command", [
|
|
609
|
+
"schemaVersion",
|
|
610
|
+
"operationId",
|
|
611
|
+
"logicalOperationKey",
|
|
612
|
+
"requestDigest",
|
|
613
|
+
"installation",
|
|
614
|
+
"route",
|
|
615
|
+
"kind",
|
|
616
|
+
"targetProviderMessageId",
|
|
617
|
+
"text",
|
|
618
|
+
]);
|
|
619
|
+
if (value.schemaVersion !== CONVERSATION_INTEGRATION_SCHEMA_VERSION) {
|
|
620
|
+
throw new TypeError("delivery command schema version is unsupported");
|
|
621
|
+
}
|
|
622
|
+
if (value.kind !== "post" && value.kind !== "update" && value.kind !== "delete") {
|
|
623
|
+
throw new TypeError("delivery command kind is unsupported");
|
|
624
|
+
}
|
|
625
|
+
assertConversationInstallationIdentity(value.installation);
|
|
626
|
+
assertConversationRoute(value.route);
|
|
627
|
+
assertSameInstallation(value.installation, value.route.installation, "delivery route");
|
|
628
|
+
const logicalOperationKey = boundedOpaqueString(
|
|
629
|
+
value.logicalOperationKey,
|
|
630
|
+
"logical operation key",
|
|
631
|
+
CONVERSATION_OPERATION_KEY_MAX_UTF8_BYTES,
|
|
632
|
+
);
|
|
633
|
+
if (
|
|
634
|
+
value.operationId !==
|
|
635
|
+
deriveConversationOperationIdentity(value.installation, logicalOperationKey)
|
|
636
|
+
) {
|
|
637
|
+
throw new TypeError("delivery operation identity is not canonical");
|
|
638
|
+
}
|
|
639
|
+
let target: ConversationProviderOpaqueId<"message"> | null;
|
|
640
|
+
let text: string | null;
|
|
641
|
+
if (value.kind === "post") {
|
|
642
|
+
if (value.targetProviderMessageId !== null) {
|
|
643
|
+
throw new TypeError("post command cannot carry a target message");
|
|
644
|
+
}
|
|
645
|
+
target = null;
|
|
646
|
+
text = boundedExactText(value.text, "delivery text", false);
|
|
647
|
+
} else {
|
|
648
|
+
assertProviderOpaqueId(value.targetProviderMessageId, "message");
|
|
649
|
+
target = value.targetProviderMessageId;
|
|
650
|
+
assertSameProvider(value.installation.provider, target.provider, "delivery target");
|
|
651
|
+
if (value.kind === "delete") {
|
|
652
|
+
if (value.text !== null) throw new TypeError("delete command cannot carry text");
|
|
653
|
+
text = null;
|
|
654
|
+
} else {
|
|
655
|
+
text = boundedExactText(value.text, "delivery text", false);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const expectedDigest = deliveryRequestDigest({
|
|
659
|
+
kind: value.kind,
|
|
660
|
+
installation: value.installation,
|
|
661
|
+
route: value.route,
|
|
662
|
+
targetProviderMessageId: target,
|
|
663
|
+
text,
|
|
664
|
+
});
|
|
665
|
+
if (value.requestDigest !== expectedDigest) {
|
|
666
|
+
throw new TypeError("delivery request digest is not canonical");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function assertConversationProviderReceipt(
|
|
671
|
+
value: unknown,
|
|
672
|
+
): asserts value is ConversationProviderReceipt {
|
|
673
|
+
assertExactObject(value, "normalized provider receipt", [
|
|
674
|
+
"schemaVersion",
|
|
675
|
+
"operationId",
|
|
676
|
+
"requestDigest",
|
|
677
|
+
"installation",
|
|
678
|
+
"providerMessageId",
|
|
679
|
+
"providerReceiptId",
|
|
680
|
+
"observedAt",
|
|
681
|
+
]);
|
|
682
|
+
if (value.schemaVersion !== CONVERSATION_INTEGRATION_SCHEMA_VERSION) {
|
|
683
|
+
throw new TypeError("provider receipt schema version is unsupported");
|
|
684
|
+
}
|
|
685
|
+
assertIdentity(value.operationId, /^ciop1_[0-9a-f]{64}$/u, "provider receipt operation id");
|
|
686
|
+
assertIdentity(value.requestDigest, /^sha256:[0-9a-f]{64}$/u, "provider receipt request digest");
|
|
687
|
+
assertConversationInstallationIdentity(value.installation);
|
|
688
|
+
assertProviderOpaqueId(value.providerMessageId, "message");
|
|
689
|
+
assertSameProvider(
|
|
690
|
+
value.installation.provider,
|
|
691
|
+
value.providerMessageId.provider,
|
|
692
|
+
"receipt message",
|
|
693
|
+
);
|
|
694
|
+
if (value.providerReceiptId !== null) {
|
|
695
|
+
assertProviderOpaqueId(value.providerReceiptId, "receipt");
|
|
696
|
+
assertSameProvider(
|
|
697
|
+
value.installation.provider,
|
|
698
|
+
value.providerReceiptId.provider,
|
|
699
|
+
"provider receipt id",
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
canonicalTimestamp(value.observedAt, "provider receipt observedAt");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function assertConversationDeliveryOutcome(
|
|
706
|
+
value: unknown,
|
|
707
|
+
): asserts value is ConversationDeliveryOutcome {
|
|
708
|
+
normalizeConversationDeliveryOutcome(value as ConversationDeliveryOutcomeInput);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function canonicalConversationInboundEnvelopeJson(
|
|
712
|
+
envelope: ConversationInboundEnvelope,
|
|
713
|
+
): string {
|
|
714
|
+
assertConversationInboundEnvelope(envelope);
|
|
715
|
+
return JSON.stringify({
|
|
716
|
+
schemaVersion: envelope.schemaVersion,
|
|
717
|
+
installation: providerIdWire(envelope.installation),
|
|
718
|
+
actor: actorWire(envelope.actor),
|
|
719
|
+
route: routeWire(envelope.route),
|
|
720
|
+
providerEventId: providerIdWire(envelope.providerEventId),
|
|
721
|
+
providerMessageId: providerIdWire(envelope.providerMessageId),
|
|
722
|
+
eventIdentity: envelope.eventIdentity,
|
|
723
|
+
occurredAt: envelope.occurredAt,
|
|
724
|
+
signal: signalWire(envelope.signal),
|
|
725
|
+
text: envelope.text,
|
|
726
|
+
attachments: envelope.attachments.map(attachmentWire),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function canonicalConversationDeliveryCommandJson(
|
|
731
|
+
command: ConversationDeliveryCommand,
|
|
732
|
+
): string {
|
|
733
|
+
assertConversationDeliveryCommand(command);
|
|
734
|
+
return JSON.stringify(deliveryCommandWire(command));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function canonicalConversationProviderReceiptJson(
|
|
738
|
+
receipt: ConversationProviderReceipt,
|
|
739
|
+
): string {
|
|
740
|
+
assertConversationProviderReceipt(receipt);
|
|
741
|
+
return JSON.stringify(providerReceiptWire(receipt));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function canonicalConversationDeliveryOutcomeJson(
|
|
745
|
+
outcome: ConversationDeliveryOutcome,
|
|
746
|
+
): string {
|
|
747
|
+
assertConversationDeliveryOutcome(outcome);
|
|
748
|
+
if (outcome.status === "success") {
|
|
749
|
+
return JSON.stringify({ status: "success", receipt: providerReceiptWire(outcome.receipt) });
|
|
750
|
+
}
|
|
751
|
+
if (outcome.status === "retryable_failure") {
|
|
752
|
+
return JSON.stringify({
|
|
753
|
+
status: outcome.status,
|
|
754
|
+
code: outcome.code,
|
|
755
|
+
retryAfterMs: outcome.retryAfterMs,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return JSON.stringify({ status: outcome.status, code: outcome.code });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function normalizeSignal(input: unknown): ConversationSignal {
|
|
762
|
+
assertExactObject(input, "conversation signal", ["kind", "control"]);
|
|
763
|
+
if (input.kind === "start" || input.kind === "continue") {
|
|
764
|
+
assertExactObject(input, "conversation signal", ["kind"]);
|
|
765
|
+
return Object.freeze({ kind: input.kind });
|
|
766
|
+
}
|
|
767
|
+
if (input.kind === "control") {
|
|
768
|
+
assertExactObject(input, "conversation control signal", ["kind", "control"]);
|
|
769
|
+
if (input.control !== "stop" && input.control !== "resume") {
|
|
770
|
+
throw new TypeError("conversation control signal is unsupported");
|
|
771
|
+
}
|
|
772
|
+
return Object.freeze({ kind: "control", control: input.control });
|
|
773
|
+
}
|
|
774
|
+
throw new TypeError("conversation signal is unsupported");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function normalizeAttachment(
|
|
778
|
+
provider: ConversationProviderNamespace,
|
|
779
|
+
input: ConversationAttachmentReferenceInput,
|
|
780
|
+
): ConversationAttachmentReference {
|
|
781
|
+
assertExactObject(input, "attachment reference", [
|
|
782
|
+
"providerAttachmentId",
|
|
783
|
+
"fileName",
|
|
784
|
+
"mediaType",
|
|
785
|
+
"byteSize",
|
|
786
|
+
"contentSha256",
|
|
787
|
+
]);
|
|
788
|
+
if (
|
|
789
|
+
ABSOLUTE_URL_PATTERN.test(input.providerAttachmentId) ||
|
|
790
|
+
input.providerAttachmentId.startsWith("//")
|
|
791
|
+
) {
|
|
792
|
+
throw new TypeError("attachment references cannot contain provider URLs");
|
|
793
|
+
}
|
|
794
|
+
const providerAttachmentId = providerOpaqueId(provider, "attachment", input.providerAttachmentId);
|
|
795
|
+
const fileName = nullableBoundedString(
|
|
796
|
+
input.fileName,
|
|
797
|
+
"attachment fileName",
|
|
798
|
+
CONVERSATION_ATTACHMENT_NAME_MAX_UTF8_BYTES,
|
|
799
|
+
);
|
|
800
|
+
if (fileName?.includes("/") || fileName?.includes("\\")) {
|
|
801
|
+
throw new TypeError("attachment fileName must not contain a path");
|
|
802
|
+
}
|
|
803
|
+
const mediaType = nullableBoundedString(
|
|
804
|
+
input.mediaType,
|
|
805
|
+
"attachment mediaType",
|
|
806
|
+
CONVERSATION_ATTACHMENT_MEDIA_TYPE_MAX_UTF8_BYTES,
|
|
807
|
+
);
|
|
808
|
+
if (mediaType !== null && !MEDIA_TYPE_PATTERN.test(mediaType)) {
|
|
809
|
+
throw new TypeError("attachment mediaType is invalid");
|
|
810
|
+
}
|
|
811
|
+
const byteSize = input.byteSize ?? null;
|
|
812
|
+
if (byteSize !== null && (!Number.isSafeInteger(byteSize) || byteSize < 0)) {
|
|
813
|
+
throw new RangeError("attachment byteSize must be a non-negative safe integer or null");
|
|
814
|
+
}
|
|
815
|
+
const contentSha256 = input.contentSha256 ?? null;
|
|
816
|
+
if (contentSha256 !== null && !SHA256_PATTERN.test(contentSha256)) {
|
|
817
|
+
throw new TypeError("attachment contentSha256 must be 64 lowercase hexadecimal characters");
|
|
818
|
+
}
|
|
819
|
+
return Object.freeze({
|
|
820
|
+
providerAttachmentId,
|
|
821
|
+
fileName,
|
|
822
|
+
mediaType,
|
|
823
|
+
byteSize,
|
|
824
|
+
contentSha256,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function providerNamespace(value: string): ConversationProviderNamespace {
|
|
829
|
+
const bounded = boundedOpaqueString(
|
|
830
|
+
value,
|
|
831
|
+
"provider namespace",
|
|
832
|
+
CONVERSATION_PROVIDER_NAMESPACE_MAX_UTF8_BYTES,
|
|
833
|
+
);
|
|
834
|
+
if (!PROVIDER_NAMESPACE_PATTERN.test(bounded)) {
|
|
835
|
+
throw new TypeError("provider namespace must be canonical lowercase segments");
|
|
836
|
+
}
|
|
837
|
+
return bounded as ConversationProviderNamespace;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function providerOpaqueId<TKind extends ConversationProviderOpaqueIdKind>(
|
|
841
|
+
provider: ConversationProviderNamespace,
|
|
842
|
+
kind: TKind,
|
|
843
|
+
value: string,
|
|
844
|
+
): ConversationProviderOpaqueId<TKind> {
|
|
845
|
+
return Object.freeze({
|
|
846
|
+
provider,
|
|
847
|
+
kind,
|
|
848
|
+
value: boundedOpaqueString(
|
|
849
|
+
value,
|
|
850
|
+
`provider ${kind} id`,
|
|
851
|
+
CONVERSATION_PROVIDER_OPAQUE_ID_MAX_UTF8_BYTES,
|
|
852
|
+
) as ConversationProviderOpaqueValue,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function deriveConversationEventIdentity(
|
|
857
|
+
installation: ConversationInstallationIdentity,
|
|
858
|
+
providerEventId: ConversationProviderOpaqueId<"event">,
|
|
859
|
+
): ConversationEventIdentity {
|
|
860
|
+
return `ciev1_${hashParts("opengeni:conversation-event:v1", [
|
|
861
|
+
installation.provider,
|
|
862
|
+
installation.value,
|
|
863
|
+
providerEventId.value,
|
|
864
|
+
])}` as ConversationEventIdentity;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function deriveConversationRouteIdentity(
|
|
868
|
+
installation: ConversationInstallationIdentity,
|
|
869
|
+
conversationId: ConversationProviderOpaqueId<"conversation">,
|
|
870
|
+
threadId: ConversationProviderOpaqueId<"thread"> | null,
|
|
871
|
+
): ConversationRouteIdentity {
|
|
872
|
+
return `cirt1_${hashParts("opengeni:conversation-route:v1", [
|
|
873
|
+
installation.provider,
|
|
874
|
+
installation.value,
|
|
875
|
+
conversationId.value,
|
|
876
|
+
threadId?.value ?? null,
|
|
877
|
+
])}` as ConversationRouteIdentity;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function deriveConversationActorIdentity(
|
|
881
|
+
installation: ConversationInstallationIdentity,
|
|
882
|
+
actorId: ConversationProviderOpaqueId<"actor">,
|
|
883
|
+
): ConversationActorIdentity {
|
|
884
|
+
return `ciac1_${hashParts("opengeni:conversation-actor:v1", [
|
|
885
|
+
installation.provider,
|
|
886
|
+
installation.value,
|
|
887
|
+
actorId.value,
|
|
888
|
+
])}` as ConversationActorIdentity;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function deriveConversationOperationIdentity(
|
|
892
|
+
installation: ConversationInstallationIdentity,
|
|
893
|
+
logicalOperationKey: string,
|
|
894
|
+
): ConversationOperationIdentity {
|
|
895
|
+
return `ciop1_${hashParts("opengeni:conversation-operation:v1", [
|
|
896
|
+
installation.provider,
|
|
897
|
+
installation.value,
|
|
898
|
+
logicalOperationKey,
|
|
899
|
+
])}` as ConversationOperationIdentity;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function deliveryRequestDigest(input: {
|
|
903
|
+
kind: ConversationDeliveryCommand["kind"];
|
|
904
|
+
installation: ConversationInstallationIdentity;
|
|
905
|
+
route: ConversationRoute;
|
|
906
|
+
targetProviderMessageId: ConversationProviderOpaqueId<"message"> | null;
|
|
907
|
+
text: string | null;
|
|
908
|
+
}): ConversationRequestDigest {
|
|
909
|
+
return `sha256:${createHash("sha256")
|
|
910
|
+
.update("opengeni:conversation-delivery-request:v1\0", "utf8")
|
|
911
|
+
.update(
|
|
912
|
+
JSON.stringify({
|
|
913
|
+
kind: input.kind,
|
|
914
|
+
installation: providerIdWire(input.installation),
|
|
915
|
+
route: routeWire(input.route),
|
|
916
|
+
targetProviderMessageId: input.targetProviderMessageId
|
|
917
|
+
? providerIdWire(input.targetProviderMessageId)
|
|
918
|
+
: null,
|
|
919
|
+
text: input.text,
|
|
920
|
+
}),
|
|
921
|
+
"utf8",
|
|
922
|
+
)
|
|
923
|
+
.digest("hex")}` as ConversationRequestDigest;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function hashParts(domain: string, parts: readonly (string | null)[]): string {
|
|
927
|
+
const hash = createHash("sha256");
|
|
928
|
+
hash.update(domain, "utf8").update("\0", "utf8");
|
|
929
|
+
for (const part of parts) {
|
|
930
|
+
if (part === null) {
|
|
931
|
+
hash.update("n:", "utf8");
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const bytes = textEncoder.encode(part);
|
|
935
|
+
hash.update(`s:${bytes.byteLength}:`, "utf8").update(bytes);
|
|
936
|
+
}
|
|
937
|
+
return hash.digest("hex");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function assertConversationInstallationIdentity(
|
|
941
|
+
value: unknown,
|
|
942
|
+
): asserts value is ConversationInstallationIdentity {
|
|
943
|
+
assertProviderOpaqueId(value, "installation");
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function assertConversationActor(value: unknown): asserts value is ConversationActor {
|
|
947
|
+
assertExactObject(value, "normalized conversation actor", [
|
|
948
|
+
"installation",
|
|
949
|
+
"providerActorId",
|
|
950
|
+
"kind",
|
|
951
|
+
"identity",
|
|
952
|
+
]);
|
|
953
|
+
assertConversationInstallationIdentity(value.installation);
|
|
954
|
+
assertProviderOpaqueId(value.providerActorId, "actor");
|
|
955
|
+
if (value.kind !== "human" && value.kind !== "bot" && value.kind !== "system") {
|
|
956
|
+
throw new TypeError("conversation actor kind is unsupported");
|
|
957
|
+
}
|
|
958
|
+
assertSameProvider(value.installation.provider, value.providerActorId.provider, "actor id");
|
|
959
|
+
if (
|
|
960
|
+
value.identity !== deriveConversationActorIdentity(value.installation, value.providerActorId)
|
|
961
|
+
) {
|
|
962
|
+
throw new TypeError("conversation actor identity is not canonical");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function assertConversationRoute(value: unknown): asserts value is ConversationRoute {
|
|
967
|
+
assertExactObject(value, "normalized conversation route", [
|
|
968
|
+
"installation",
|
|
969
|
+
"providerConversationId",
|
|
970
|
+
"providerThreadId",
|
|
971
|
+
"identity",
|
|
972
|
+
]);
|
|
973
|
+
assertConversationInstallationIdentity(value.installation);
|
|
974
|
+
assertProviderOpaqueId(value.providerConversationId, "conversation");
|
|
975
|
+
if (value.providerThreadId !== null) assertProviderOpaqueId(value.providerThreadId, "thread");
|
|
976
|
+
assertSameProvider(
|
|
977
|
+
value.installation.provider,
|
|
978
|
+
value.providerConversationId.provider,
|
|
979
|
+
"conversation id",
|
|
980
|
+
);
|
|
981
|
+
if (value.providerThreadId) {
|
|
982
|
+
assertSameProvider(value.installation.provider, value.providerThreadId.provider, "thread id");
|
|
983
|
+
}
|
|
984
|
+
if (
|
|
985
|
+
value.identity !==
|
|
986
|
+
deriveConversationRouteIdentity(
|
|
987
|
+
value.installation,
|
|
988
|
+
value.providerConversationId,
|
|
989
|
+
value.providerThreadId,
|
|
990
|
+
)
|
|
991
|
+
) {
|
|
992
|
+
throw new TypeError("conversation route identity is not canonical");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function assertConversationAttachmentReference(
|
|
997
|
+
value: unknown,
|
|
998
|
+
): asserts value is ConversationAttachmentReference {
|
|
999
|
+
assertExactObject(value, "normalized attachment reference", [
|
|
1000
|
+
"providerAttachmentId",
|
|
1001
|
+
"fileName",
|
|
1002
|
+
"mediaType",
|
|
1003
|
+
"byteSize",
|
|
1004
|
+
"contentSha256",
|
|
1005
|
+
]);
|
|
1006
|
+
assertProviderOpaqueId(value.providerAttachmentId, "attachment");
|
|
1007
|
+
if (
|
|
1008
|
+
ABSOLUTE_URL_PATTERN.test(value.providerAttachmentId.value) ||
|
|
1009
|
+
value.providerAttachmentId.value.startsWith("//")
|
|
1010
|
+
) {
|
|
1011
|
+
throw new TypeError("attachment references cannot contain provider URLs");
|
|
1012
|
+
}
|
|
1013
|
+
const fileName = nullableBoundedString(
|
|
1014
|
+
value.fileName,
|
|
1015
|
+
"attachment fileName",
|
|
1016
|
+
CONVERSATION_ATTACHMENT_NAME_MAX_UTF8_BYTES,
|
|
1017
|
+
);
|
|
1018
|
+
if (fileName?.includes("/") || fileName?.includes("\\")) {
|
|
1019
|
+
throw new TypeError("attachment fileName must not contain a path");
|
|
1020
|
+
}
|
|
1021
|
+
const mediaType = nullableBoundedString(
|
|
1022
|
+
value.mediaType,
|
|
1023
|
+
"attachment mediaType",
|
|
1024
|
+
CONVERSATION_ATTACHMENT_MEDIA_TYPE_MAX_UTF8_BYTES,
|
|
1025
|
+
);
|
|
1026
|
+
if (mediaType !== null && !MEDIA_TYPE_PATTERN.test(mediaType)) {
|
|
1027
|
+
throw new TypeError("attachment mediaType is invalid");
|
|
1028
|
+
}
|
|
1029
|
+
const byteSize = value.byteSize;
|
|
1030
|
+
if (byteSize !== null) {
|
|
1031
|
+
if (typeof byteSize !== "number" || !Number.isSafeInteger(byteSize) || byteSize < 0) {
|
|
1032
|
+
throw new RangeError("attachment byteSize must be a non-negative safe integer or null");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const contentSha256 = value.contentSha256;
|
|
1036
|
+
if (contentSha256 !== null) {
|
|
1037
|
+
if (typeof contentSha256 !== "string" || !SHA256_PATTERN.test(contentSha256)) {
|
|
1038
|
+
throw new TypeError("attachment contentSha256 must be 64 lowercase hexadecimal characters");
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function assertProviderOpaqueId<TKind extends ConversationProviderOpaqueIdKind>(
|
|
1044
|
+
value: unknown,
|
|
1045
|
+
kind: TKind,
|
|
1046
|
+
): asserts value is ConversationProviderOpaqueId<TKind> {
|
|
1047
|
+
assertExactObject(value, `provider ${kind} id`, ["provider", "kind", "value"]);
|
|
1048
|
+
providerNamespace(value.provider as string);
|
|
1049
|
+
if (value.kind !== kind) throw new TypeError(`provider id must have kind ${kind}`);
|
|
1050
|
+
boundedOpaqueString(
|
|
1051
|
+
value.value,
|
|
1052
|
+
`provider ${kind} id`,
|
|
1053
|
+
CONVERSATION_PROVIDER_OPAQUE_ID_MAX_UTF8_BYTES,
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function assertSameProvider(
|
|
1058
|
+
expected: ConversationProviderNamespace,
|
|
1059
|
+
actual: ConversationProviderNamespace,
|
|
1060
|
+
label: string,
|
|
1061
|
+
): void {
|
|
1062
|
+
if (expected !== actual) throw new TypeError(`${label} provider namespace does not match`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function assertSameInstallation(
|
|
1066
|
+
expected: ConversationInstallationIdentity,
|
|
1067
|
+
actual: ConversationInstallationIdentity,
|
|
1068
|
+
label: string,
|
|
1069
|
+
): void {
|
|
1070
|
+
if (expected.provider !== actual.provider || expected.value !== actual.value) {
|
|
1071
|
+
throw new TypeError(`${label} installation identity does not match`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function boundedExactText(value: unknown, label: string, allowEmpty: boolean): string {
|
|
1076
|
+
return boundedString(value, label, allowEmpty ? 0 : 1, CONVERSATION_TEXT_MAX_UTF8_BYTES, true);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function boundedOpaqueString(value: unknown, label: string, maxBytes: number): string {
|
|
1080
|
+
const result = boundedString(value, label, 1, maxBytes, false);
|
|
1081
|
+
if (result !== result.trim())
|
|
1082
|
+
throw new TypeError(`${label} must not have surrounding whitespace`);
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function nullableBoundedString(value: unknown, label: string, maxBytes: number): string | null {
|
|
1087
|
+
if (value === undefined || value === null) return null;
|
|
1088
|
+
return boundedString(value, label, 1, maxBytes, false);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function boundedString(
|
|
1092
|
+
value: unknown,
|
|
1093
|
+
label: string,
|
|
1094
|
+
minBytes: number,
|
|
1095
|
+
maxBytes: number,
|
|
1096
|
+
allowNewlines: boolean,
|
|
1097
|
+
): string {
|
|
1098
|
+
if (typeof value !== "string") throw new TypeError(`${label} must be a string`);
|
|
1099
|
+
if (!isWellFormedUnicode(value)) throw new TypeError(`${label} must be well-formed Unicode`);
|
|
1100
|
+
if (value.includes("\0")) throw new TypeError(`${label} must not contain NUL`);
|
|
1101
|
+
if (!allowNewlines && /[\u0001-\u001f\u007f]/u.test(value)) {
|
|
1102
|
+
throw new TypeError(`${label} must not contain control characters`);
|
|
1103
|
+
}
|
|
1104
|
+
const byteLength = textEncoder.encode(value).byteLength;
|
|
1105
|
+
if (byteLength < minBytes || byteLength > maxBytes) {
|
|
1106
|
+
throw new RangeError(`${label} must contain ${minBytes}-${maxBytes} UTF-8 bytes`);
|
|
1107
|
+
}
|
|
1108
|
+
return value;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function isWellFormedUnicode(value: string): boolean {
|
|
1112
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1113
|
+
const code = value.charCodeAt(index);
|
|
1114
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
1115
|
+
const next = value.charCodeAt(index + 1);
|
|
1116
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
1117
|
+
index += 1;
|
|
1118
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function canonicalTimestamp(value: unknown, label: string): string {
|
|
1126
|
+
const timestamp = boundedString(value, label, 20, 32, false);
|
|
1127
|
+
const milliseconds = Date.parse(timestamp);
|
|
1128
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString() !== timestamp) {
|
|
1129
|
+
throw new TypeError(`${label} must be a canonical UTC timestamp`);
|
|
1130
|
+
}
|
|
1131
|
+
return timestamp;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function outcomeCode(value: unknown): string {
|
|
1135
|
+
const code = boundedOpaqueString(
|
|
1136
|
+
value,
|
|
1137
|
+
"delivery outcome code",
|
|
1138
|
+
CONVERSATION_OUTCOME_CODE_MAX_UTF8_BYTES,
|
|
1139
|
+
);
|
|
1140
|
+
if (!OUTCOME_CODE_PATTERN.test(code)) {
|
|
1141
|
+
throw new TypeError("delivery outcome code must use canonical lowercase segments");
|
|
1142
|
+
}
|
|
1143
|
+
return code;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function assertExactObject(
|
|
1147
|
+
value: unknown,
|
|
1148
|
+
label: string,
|
|
1149
|
+
allowedKeys: readonly string[],
|
|
1150
|
+
): asserts value is Record<string, unknown> {
|
|
1151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1152
|
+
throw new TypeError(`${label} must be an object`);
|
|
1153
|
+
}
|
|
1154
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1155
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1156
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
1157
|
+
}
|
|
1158
|
+
const allowed = new Set(allowedKeys);
|
|
1159
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1160
|
+
if (typeof key !== "string" || !allowed.has(key)) {
|
|
1161
|
+
throw new TypeError(`${label} contains an unsupported field`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function assertIdentity(value: unknown, pattern: RegExp, label: string): asserts value is string {
|
|
1167
|
+
if (typeof value !== "string" || !pattern.test(value)) {
|
|
1168
|
+
throw new TypeError(`${label} is invalid`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function providerIdWire<TKind extends ConversationProviderOpaqueIdKind>(
|
|
1173
|
+
id: ConversationProviderOpaqueId<TKind>,
|
|
1174
|
+
): { provider: string; kind: TKind; value: string } {
|
|
1175
|
+
return { provider: id.provider, kind: id.kind, value: id.value };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function actorWire(actor: ConversationActor) {
|
|
1179
|
+
return {
|
|
1180
|
+
installation: providerIdWire(actor.installation),
|
|
1181
|
+
providerActorId: providerIdWire(actor.providerActorId),
|
|
1182
|
+
kind: actor.kind,
|
|
1183
|
+
identity: actor.identity,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function routeWire(route: ConversationRoute) {
|
|
1188
|
+
return {
|
|
1189
|
+
installation: providerIdWire(route.installation),
|
|
1190
|
+
providerConversationId: providerIdWire(route.providerConversationId),
|
|
1191
|
+
providerThreadId: route.providerThreadId ? providerIdWire(route.providerThreadId) : null,
|
|
1192
|
+
identity: route.identity,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function signalWire(signal: ConversationSignal) {
|
|
1197
|
+
return signal.kind === "control"
|
|
1198
|
+
? { kind: "control" as const, control: signal.control }
|
|
1199
|
+
: { kind: signal.kind };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function attachmentWire(attachment: ConversationAttachmentReference) {
|
|
1203
|
+
return {
|
|
1204
|
+
providerAttachmentId: providerIdWire(attachment.providerAttachmentId),
|
|
1205
|
+
fileName: attachment.fileName,
|
|
1206
|
+
mediaType: attachment.mediaType,
|
|
1207
|
+
byteSize: attachment.byteSize,
|
|
1208
|
+
contentSha256: attachment.contentSha256,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function deliveryCommandWire(command: ConversationDeliveryCommand) {
|
|
1213
|
+
return {
|
|
1214
|
+
schemaVersion: command.schemaVersion,
|
|
1215
|
+
operationId: command.operationId,
|
|
1216
|
+
logicalOperationKey: command.logicalOperationKey,
|
|
1217
|
+
requestDigest: command.requestDigest,
|
|
1218
|
+
installation: providerIdWire(command.installation),
|
|
1219
|
+
route: routeWire(command.route),
|
|
1220
|
+
kind: command.kind,
|
|
1221
|
+
targetProviderMessageId: command.targetProviderMessageId
|
|
1222
|
+
? providerIdWire(command.targetProviderMessageId)
|
|
1223
|
+
: null,
|
|
1224
|
+
text: command.text,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function providerReceiptWire(receipt: ConversationProviderReceipt) {
|
|
1229
|
+
return {
|
|
1230
|
+
schemaVersion: receipt.schemaVersion,
|
|
1231
|
+
operationId: receipt.operationId,
|
|
1232
|
+
requestDigest: receipt.requestDigest,
|
|
1233
|
+
installation: providerIdWire(receipt.installation),
|
|
1234
|
+
providerMessageId: providerIdWire(receipt.providerMessageId),
|
|
1235
|
+
providerReceiptId: receipt.providerReceiptId ? providerIdWire(receipt.providerReceiptId) : null,
|
|
1236
|
+
observedAt: receipt.observedAt,
|
|
1237
|
+
};
|
|
1238
|
+
}
|