@flow-as-code/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +30 -0
  3. package/SPEC.md +92 -0
  4. package/dist/actions.d.ts +87 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +204 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/aws.d.ts +27 -0
  9. package/dist/aws.d.ts.map +1 -0
  10. package/dist/aws.js +32 -0
  11. package/dist/aws.js.map +1 -0
  12. package/dist/blocks.d.ts +287 -0
  13. package/dist/blocks.d.ts.map +1 -0
  14. package/dist/blocks.js +352 -0
  15. package/dist/blocks.js.map +1 -0
  16. package/dist/codegen.d.ts +21 -0
  17. package/dist/codegen.d.ts.map +1 -0
  18. package/dist/codegen.js +1027 -0
  19. package/dist/codegen.js.map +1 -0
  20. package/dist/export.d.ts +256 -0
  21. package/dist/export.d.ts.map +1 -0
  22. package/dist/export.js +799 -0
  23. package/dist/export.js.map +1 -0
  24. package/dist/flow.d.ts +41 -0
  25. package/dist/flow.d.ts.map +1 -0
  26. package/dist/flow.js +85 -0
  27. package/dist/flow.js.map +1 -0
  28. package/dist/flowdoc.d.ts +111 -0
  29. package/dist/flowdoc.d.ts.map +1 -0
  30. package/dist/flowdoc.js +139 -0
  31. package/dist/flowdoc.js.map +1 -0
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +26 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/layout.d.ts +11 -0
  37. package/dist/layout.d.ts.map +1 -0
  38. package/dist/layout.js +49 -0
  39. package/dist/layout.js.map +1 -0
  40. package/dist/lint/engine.d.ts +16 -0
  41. package/dist/lint/engine.d.ts.map +1 -0
  42. package/dist/lint/engine.js +37 -0
  43. package/dist/lint/engine.js.map +1 -0
  44. package/dist/lint/graph.d.ts +34 -0
  45. package/dist/lint/graph.d.ts.map +1 -0
  46. package/dist/lint/graph.js +79 -0
  47. package/dist/lint/graph.js.map +1 -0
  48. package/dist/lint/index.d.ts +7 -0
  49. package/dist/lint/index.d.ts.map +1 -0
  50. package/dist/lint/index.js +5 -0
  51. package/dist/lint/index.js.map +1 -0
  52. package/dist/lint/reporters.d.ts +6 -0
  53. package/dist/lint/reporters.d.ts.map +1 -0
  54. package/dist/lint/reporters.js +30 -0
  55. package/dist/lint/reporters.js.map +1 -0
  56. package/dist/lint/rules/action-allowed-in-flow-type.d.ts +11 -0
  57. package/dist/lint/rules/action-allowed-in-flow-type.d.ts.map +1 -0
  58. package/dist/lint/rules/action-allowed-in-flow-type.js +32 -0
  59. package/dist/lint/rules/action-allowed-in-flow-type.js.map +1 -0
  60. package/dist/lint/rules/action-count.d.ts +16 -0
  61. package/dist/lint/rules/action-count.d.ts.map +1 -0
  62. package/dist/lint/rules/action-count.js +32 -0
  63. package/dist/lint/rules/action-count.js.map +1 -0
  64. package/dist/lint/rules/error-branches.d.ts +10 -0
  65. package/dist/lint/rules/error-branches.d.ts.map +1 -0
  66. package/dist/lint/rules/error-branches.js +39 -0
  67. package/dist/lint/rules/error-branches.js.map +1 -0
  68. package/dist/lint/rules/index.d.ts +17 -0
  69. package/dist/lint/rules/index.d.ts.map +1 -0
  70. package/dist/lint/rules/index.js +36 -0
  71. package/dist/lint/rules/index.js.map +1 -0
  72. package/dist/lint/rules/module-depth-5.d.ts +14 -0
  73. package/dist/lint/rules/module-depth-5.d.ts.map +1 -0
  74. package/dist/lint/rules/module-depth-5.js +67 -0
  75. package/dist/lint/rules/module-depth-5.js.map +1 -0
  76. package/dist/lint/rules/no-literal-arn.d.ts +21 -0
  77. package/dist/lint/rules/no-literal-arn.d.ts.map +1 -0
  78. package/dist/lint/rules/no-literal-arn.js +58 -0
  79. package/dist/lint/rules/no-literal-arn.js.map +1 -0
  80. package/dist/lint/rules/no-unresolved-token.d.ts +8 -0
  81. package/dist/lint/rules/no-unresolved-token.d.ts.map +1 -0
  82. package/dist/lint/rules/no-unresolved-token.js +71 -0
  83. package/dist/lint/rules/no-unresolved-token.js.map +1 -0
  84. package/dist/lint/rules/prompt-length-3000.d.ts +16 -0
  85. package/dist/lint/rules/prompt-length-3000.d.ts.map +1 -0
  86. package/dist/lint/rules/prompt-length-3000.js +61 -0
  87. package/dist/lint/rules/prompt-length-3000.js.map +1 -0
  88. package/dist/lint/rules/reachable-blocks.d.ts +7 -0
  89. package/dist/lint/rules/reachable-blocks.d.ts.map +1 -0
  90. package/dist/lint/rules/reachable-blocks.js +45 -0
  91. package/dist/lint/rules/reachable-blocks.js.map +1 -0
  92. package/dist/lint/rules/recording-consent-before-record.d.ts +3 -0
  93. package/dist/lint/rules/recording-consent-before-record.d.ts.map +1 -0
  94. package/dist/lint/rules/recording-consent-before-record.js +87 -0
  95. package/dist/lint/rules/recording-consent-before-record.js.map +1 -0
  96. package/dist/lint/rules/terminal-blocks.d.ts +8 -0
  97. package/dist/lint/rules/terminal-blocks.d.ts.map +1 -0
  98. package/dist/lint/rules/terminal-blocks.js +40 -0
  99. package/dist/lint/rules/terminal-blocks.js.map +1 -0
  100. package/dist/lint/rules/unique-names.d.ts +7 -0
  101. package/dist/lint/rules/unique-names.d.ts.map +1 -0
  102. package/dist/lint/rules/unique-names.js +40 -0
  103. package/dist/lint/rules/unique-names.js.map +1 -0
  104. package/dist/lint/types.d.ts +30 -0
  105. package/dist/lint/types.d.ts.map +1 -0
  106. package/dist/lint/types.js +11 -0
  107. package/dist/lint/types.js.map +1 -0
  108. package/dist/materialize.d.ts +31 -0
  109. package/dist/materialize.d.ts.map +1 -0
  110. package/dist/materialize.js +192 -0
  111. package/dist/materialize.js.map +1 -0
  112. package/dist/package-names.d.ts +11 -0
  113. package/dist/package-names.d.ts.map +1 -0
  114. package/dist/package-names.js +25 -0
  115. package/dist/package-names.js.map +1 -0
  116. package/dist/refs.d.ts +35 -0
  117. package/dist/refs.d.ts.map +1 -0
  118. package/dist/refs.js +69 -0
  119. package/dist/refs.js.map +1 -0
  120. package/dist/serialize.d.ts +13 -0
  121. package/dist/serialize.d.ts.map +1 -0
  122. package/dist/serialize.js +86 -0
  123. package/dist/serialize.js.map +1 -0
  124. package/dist/simulate.d.ts +446 -0
  125. package/dist/simulate.d.ts.map +1 -0
  126. package/dist/simulate.js +1115 -0
  127. package/dist/simulate.js.map +1 -0
  128. package/dist/synth.d.ts +25 -0
  129. package/dist/synth.d.ts.map +1 -0
  130. package/dist/synth.js +93 -0
  131. package/dist/synth.js.map +1 -0
  132. package/package.json +73 -0
  133. package/src/actions.ts +221 -0
  134. package/src/aws.ts +55 -0
  135. package/src/blocks.ts +619 -0
  136. package/src/codegen.ts +1144 -0
  137. package/src/export.ts +1190 -0
  138. package/src/flow.ts +108 -0
  139. package/src/flowdoc.ts +261 -0
  140. package/src/index.ts +31 -0
  141. package/src/layout.ts +54 -0
  142. package/src/lint/engine.ts +56 -0
  143. package/src/lint/graph.ts +86 -0
  144. package/src/lint/index.ts +10 -0
  145. package/src/lint/reporters.ts +44 -0
  146. package/src/lint/rules/action-allowed-in-flow-type.ts +32 -0
  147. package/src/lint/rules/action-count.ts +35 -0
  148. package/src/lint/rules/error-branches.ts +45 -0
  149. package/src/lint/rules/index.ts +53 -0
  150. package/src/lint/rules/module-depth-5.ts +70 -0
  151. package/src/lint/rules/no-literal-arn.ts +64 -0
  152. package/src/lint/rules/no-unresolved-token.ts +80 -0
  153. package/src/lint/rules/prompt-length-3000.ts +66 -0
  154. package/src/lint/rules/reachable-blocks.ts +50 -0
  155. package/src/lint/rules/recording-consent-before-record.ts +90 -0
  156. package/src/lint/rules/terminal-blocks.ts +45 -0
  157. package/src/lint/rules/unique-names.ts +44 -0
  158. package/src/lint/types.ts +43 -0
  159. package/src/materialize.ts +230 -0
  160. package/src/package-names.ts +26 -0
  161. package/src/refs.ts +93 -0
  162. package/src/serialize.ts +97 -0
  163. package/src/simulate.ts +1610 -0
  164. package/src/synth.ts +110 -0
package/src/blocks.ts ADDED
@@ -0,0 +1,619 @@
1
+ /*
2
+ * Copyright 2026 The flow-as-code Authors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ // Builder blocks.
6
+ //
7
+ // Blocks map one-to-one onto Connect Actions. That is deliberate: the
8
+ // round-trip invariant (synth(codegen(doc)) equals doc) is a non-negotiable,
9
+ // and a block that expanded into several Actions could not be recognised again
10
+ // on the way back. So "transfer to queue", which needs both an
11
+ // UpdateContactTargetQueue and a TransferContactToQueue, is two blocks.
12
+ //
13
+ // Error branches are required properties on a config object rather than
14
+ // something enforced by a fluent builder's type state. Omitting one is a real
15
+ // compile-time error, and a plain object literal is something codegen can emit
16
+ // as idiomatic, diffable TypeScript. See docs/adr/0002-error-branch-enforcement.md.
17
+
18
+ import {
19
+ ActionType,
20
+ DTMF_DIGITS,
21
+ EXTRA_ERRORS,
22
+ INPUT_TIME_LIMIT_EXCEEDED,
23
+ INPUT_TIMEOUT_MAX,
24
+ INPUT_TIMEOUT_MIN,
25
+ LAMBDA_TIMEOUT_MAX,
26
+ LAMBDA_TIMEOUT_MIN,
27
+ NO_MATCHING_CONDITION,
28
+ NO_MATCHING_ERROR,
29
+ } from "./actions.js";
30
+ import type { DtmfDigit } from "./actions.js";
31
+ import type { Condition, ConditionOperator, FlowAction, Transitions } from "./flowdoc.js";
32
+ import { isValidIdentifier } from "./flowdoc.js";
33
+ import type { JsonPath, Ref } from "./refs.js";
34
+
35
+ /** A transition target: another block, or its Identifier. */
36
+ export type Target = string | Block;
37
+
38
+ export function targetId(t: Target): string {
39
+ return typeof t === "string" ? t : t.id;
40
+ }
41
+
42
+ export abstract class Block {
43
+ readonly id: string;
44
+
45
+ constructor(id: string) {
46
+ if (!isValidIdentifier(id)) {
47
+ throw new Error(
48
+ `Invalid Identifier "${id}". Must be 1 to 50 characters and must not contain % : ( \\ / ) = $ , ; [ ] { }.`,
49
+ );
50
+ }
51
+ this.id = id;
52
+ }
53
+
54
+ abstract readonly type: string;
55
+ protected abstract parameters(): Record<string, unknown>;
56
+ protected abstract transitions(): Transitions;
57
+
58
+ toAction(): FlowAction {
59
+ return {
60
+ Identifier: this.id,
61
+ Type: this.type,
62
+ Parameters: this.parameters(),
63
+ Transitions: this.transitions(),
64
+ };
65
+ }
66
+ }
67
+
68
+ /** Shared shape for a block with a success path and a catch-all error path. */
69
+ interface Wired {
70
+ id: string;
71
+ next: Target;
72
+ onError: Target;
73
+ }
74
+
75
+ function wire(
76
+ next: Target | undefined,
77
+ errors: [string, Target][],
78
+ conditions: ConditionTransitionInput[] = [],
79
+ ): Transitions {
80
+ const t: Transitions = {};
81
+ if (next !== undefined) t.NextAction = targetId(next);
82
+ t.Errors = errors.map(([ErrorType, target]) => ({ ErrorType, NextAction: targetId(target) }));
83
+ t.Conditions = conditions.map((c) => ({
84
+ NextAction: targetId(c.target),
85
+ Condition: { Operator: c.operator, Operands: c.operands },
86
+ }));
87
+ return t;
88
+ }
89
+
90
+ interface ConditionTransitionInput {
91
+ target: Target;
92
+ operator: ConditionOperator;
93
+ operands: string[];
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Participant actions
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * MessageParticipant accepts exactly one of Text, SSML, or PromptId. The union
102
+ * makes supplying two a compile-time error.
103
+ * PromptId and SSML are voice only; other channels support only Text.
104
+ */
105
+ export type MessageBody =
106
+ | { text: string; ssml?: never; prompt?: never }
107
+ | { ssml: string; text?: never; prompt?: never }
108
+ | { prompt: Ref<"prompt"> | JsonPath; text?: never; ssml?: never };
109
+
110
+ export type MessageParticipantConfig = Wired & MessageBody;
111
+
112
+ export class MessageParticipant extends Block {
113
+ readonly type = ActionType.MessageParticipant;
114
+
115
+ constructor(private readonly config: MessageParticipantConfig) {
116
+ super(config.id);
117
+ }
118
+
119
+ protected parameters(): Record<string, unknown> {
120
+ const { text, ssml, prompt } = this.config;
121
+ if (text !== undefined) return { Text: text };
122
+ if (ssml !== undefined) return { SSML: ssml };
123
+ return { PromptId: prompt };
124
+ }
125
+
126
+ protected transitions(): Transitions {
127
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
128
+ }
129
+ }
130
+
131
+ /** One key of a DTMF menu and where it leads. */
132
+ export interface DtmfBranch {
133
+ digit: DtmfDigit;
134
+ target: Target;
135
+ }
136
+
137
+ /**
138
+ * A DTMF menu: play something, wait for one key, branch on it.
139
+ *
140
+ * GetParticipantInput has two forms. With StoreInput "False" the key pressed
141
+ * is the run result and Conditions branch on it; conditions "may use only the
142
+ * Equals operator" and each operand "must be static and be a single character
143
+ * - 0-9 numeric, *, or #". With StoreInput "True" the digits are stored,
144
+ * InputValidation is required, and there are no conditions. The builder models
145
+ * the menu form only; the stored-input form, and anything carrying Media,
146
+ * InputEncryption, or DTMFConfiguration, parses to a GenericBlock and
147
+ * round-trips verbatim.
148
+ * https://docs.aws.amazon.com/connect/latest/devguide/participant-actions-getparticipantinput.html
149
+ *
150
+ * The prompt is optional: PromptId, Text, and SSML are each "[Optional]" on
151
+ * the action page, and a menu may follow a prompt played by an earlier block.
152
+ *
153
+ * Every error the menu form documents is a required property: onTimeout
154
+ * (InputTimeLimitExceeded), onNoMatch (NoMatchingCondition), and onError
155
+ * (NoMatchingError). NextAction mirrors onNoMatch, the way
156
+ * CheckHoursOfOperation mirrors its out-of-hours path.
157
+ *
158
+ * InputTimeLimitSeconds and StoreInput are emitted as JSON strings because
159
+ * that is how the console writes them; the admin page's Flow language example
160
+ * has "InputTimeLimitSeconds": "5" and "StoreInput": "False". (InvokeLambda's
161
+ * timeout is a number for the same reason: its page shows a number.)
162
+ * https://docs.aws.amazon.com/connect/latest/adminguide/get-customer-input.html
163
+ */
164
+ export type GetParticipantInputConfig = {
165
+ id: string;
166
+ /**
167
+ * Seconds to wait for the first key. Static integer, 1 to 180 inclusive
168
+ * (INPUT_TIMEOUT_MIN and INPUT_TIMEOUT_MAX).
169
+ */
170
+ timeoutSeconds: number;
171
+ /**
172
+ * Keys that branch, in the order Connect evaluates them. May be empty. Each
173
+ * key may branch once; a repeated key is refused by the constructor.
174
+ */
175
+ branches: DtmfBranch[];
176
+ onTimeout: Target;
177
+ onNoMatch: Target;
178
+ onError: Target;
179
+ } & (MessageBody | { text?: never; ssml?: never; prompt?: never });
180
+
181
+ export class GetParticipantInput extends Block {
182
+ readonly type = ActionType.GetParticipantInput;
183
+
184
+ constructor(private readonly config: GetParticipantInputConfig) {
185
+ super(config.id);
186
+ const t = config.timeoutSeconds;
187
+ if (!Number.isInteger(t) || t < INPUT_TIMEOUT_MIN || t > INPUT_TIMEOUT_MAX) {
188
+ throw new Error(
189
+ `GetParticipantInput "${config.id}" timeoutSeconds must be an integer between ${INPUT_TIMEOUT_MIN} and ${INPUT_TIMEOUT_MAX}, got ${t}.`,
190
+ );
191
+ }
192
+ // A key that branches twice is two answers to one press. Only one of
193
+ // them can be taken, so the class refuses the config rather than emit
194
+ // both conditions and leave the caller believing each branch is live.
195
+ // codegen constructs the real block to verify an inversion, so a document
196
+ // carrying a repeated key stays a GenericBlock instead of generating a
197
+ // call that throws.
198
+ const seen = new Set<string>();
199
+ for (const b of config.branches) {
200
+ if (!(DTMF_DIGITS as readonly string[]).includes(b.digit)) {
201
+ throw new Error(
202
+ `GetParticipantInput "${config.id}" branch digit must be one of ${DTMF_DIGITS.join(" ")}, got "${String(b.digit)}".`,
203
+ );
204
+ }
205
+ if (seen.has(b.digit)) {
206
+ throw new Error(
207
+ `GetParticipantInput "${config.id}" branches on key "${b.digit}" twice; each key may branch once.`,
208
+ );
209
+ }
210
+ seen.add(b.digit);
211
+ }
212
+ }
213
+
214
+ protected parameters(): Record<string, unknown> {
215
+ const { text, ssml, prompt } = this.config;
216
+ const p: Record<string, unknown> = {};
217
+ if (text !== undefined) p.Text = text;
218
+ else if (ssml !== undefined) p.SSML = ssml;
219
+ else if (prompt !== undefined) p.PromptId = prompt;
220
+ p.InputTimeLimitSeconds = String(this.config.timeoutSeconds);
221
+ p.StoreInput = "False";
222
+ return p;
223
+ }
224
+
225
+ protected transitions(): Transitions {
226
+ const { onTimeout, onNoMatch, onError } = this.config;
227
+ return wire(
228
+ onNoMatch,
229
+ [
230
+ [INPUT_TIME_LIMIT_EXCEEDED, onTimeout],
231
+ [NO_MATCHING_CONDITION, onNoMatch],
232
+ [NO_MATCHING_ERROR, onError],
233
+ ],
234
+ this.config.branches.map((b) => ({
235
+ target: b.target,
236
+ operator: "Equals",
237
+ operands: [b.digit],
238
+ })),
239
+ );
240
+ }
241
+ }
242
+
243
+ /** Terminal. Takes no parameters and supports no errors. */
244
+ export class DisconnectParticipant extends Block {
245
+ readonly type = ActionType.DisconnectParticipant;
246
+
247
+ constructor(config: { id: string }) {
248
+ super(config.id);
249
+ }
250
+
251
+ protected parameters(): Record<string, unknown> {
252
+ return {};
253
+ }
254
+
255
+ protected transitions(): Transitions {
256
+ return {};
257
+ }
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Flow control actions
262
+ // ---------------------------------------------------------------------------
263
+
264
+ /**
265
+ * Connect requires exactly two conditions on this action, Equals True and
266
+ * Equals False, and no others. Both are therefore required config, not
267
+ * something the caller wires by hand.
268
+ */
269
+ export interface CheckHoursOfOperationConfig {
270
+ id: string;
271
+ /** Optional. When absent, the contact's TargetQueue hours are used. */
272
+ hours?: Ref<"hours"> | JsonPath;
273
+ onInHours: Target;
274
+ onOutOfHours: Target;
275
+ onError: Target;
276
+ }
277
+
278
+ export class CheckHoursOfOperation extends Block {
279
+ readonly type = ActionType.CheckHoursOfOperation;
280
+
281
+ constructor(private readonly config: CheckHoursOfOperationConfig) {
282
+ super(config.id);
283
+ }
284
+
285
+ protected parameters(): Record<string, unknown> {
286
+ return this.config.hours === undefined ? {} : { HoursOfOperationId: this.config.hours };
287
+ }
288
+
289
+ protected transitions(): Transitions {
290
+ // NextAction is the fallback when no condition matches. The conditions are
291
+ // exhaustive, so it mirrors the out-of-hours path.
292
+ return wire(
293
+ this.config.onOutOfHours,
294
+ [[NO_MATCHING_ERROR, this.config.onError]],
295
+ [
296
+ { target: this.config.onInHours, operator: "Equals", operands: ["True"] },
297
+ { target: this.config.onOutOfHours, operator: "Equals", operands: ["False"] },
298
+ ],
299
+ );
300
+ }
301
+ }
302
+
303
+ export interface CompareBranch {
304
+ operator: ConditionOperator;
305
+ operands: string[];
306
+ target: Target;
307
+ }
308
+
309
+ /** Compare fails with NoMatchingCondition, the only modeled action that does. */
310
+ export interface CompareConfig {
311
+ id: string;
312
+ value: JsonPath;
313
+ branches: CompareBranch[];
314
+ onNoMatch: Target;
315
+ }
316
+
317
+ export class Compare extends Block {
318
+ readonly type = ActionType.Compare;
319
+
320
+ constructor(private readonly config: CompareConfig) {
321
+ super(config.id);
322
+ if (config.branches.length === 0) {
323
+ throw new Error(`Compare "${config.id}" needs at least one branch.`);
324
+ }
325
+ }
326
+
327
+ protected parameters(): Record<string, unknown> {
328
+ return { ComparisonValue: this.config.value };
329
+ }
330
+
331
+ protected transitions(): Transitions {
332
+ return wire(
333
+ undefined,
334
+ [[NO_MATCHING_CONDITION, this.config.onNoMatch]],
335
+ this.config.branches.map((b) => ({
336
+ target: b.target,
337
+ operator: b.operator,
338
+ operands: b.operands,
339
+ })),
340
+ );
341
+ }
342
+ }
343
+
344
+ export interface TransferToFlowConfig extends Wired {
345
+ flow: Ref<"flow"> | JsonPath;
346
+ }
347
+
348
+ export class TransferToFlow extends Block {
349
+ readonly type = ActionType.TransferToFlow;
350
+
351
+ constructor(private readonly config: TransferToFlowConfig) {
352
+ super(config.id);
353
+ }
354
+
355
+ protected parameters(): Record<string, unknown> {
356
+ return { ContactFlowId: this.config.flow };
357
+ }
358
+
359
+ protected transitions(): Transitions {
360
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
361
+ }
362
+ }
363
+
364
+ /** Terminal. Only legal in whisper and customer queue flows. */
365
+ export class EndFlowExecution extends Block {
366
+ readonly type = ActionType.EndFlowExecution;
367
+
368
+ constructor(config: { id: string }) {
369
+ super(config.id);
370
+ }
371
+
372
+ protected parameters(): Record<string, unknown> {
373
+ return {};
374
+ }
375
+
376
+ protected transitions(): Transitions {
377
+ return {};
378
+ }
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Contact actions
383
+ // ---------------------------------------------------------------------------
384
+
385
+ /** Accepts a queue or an agent queue, never both. */
386
+ export type QueueTarget =
387
+ | { queue: Ref<"queue"> | JsonPath; agent?: never }
388
+ | { agent: Ref<"queue"> | JsonPath; queue?: never };
389
+
390
+ export type UpdateContactTargetQueueConfig = Wired & QueueTarget;
391
+
392
+ export class UpdateContactTargetQueue extends Block {
393
+ readonly type = ActionType.UpdateContactTargetQueue;
394
+
395
+ constructor(private readonly config: UpdateContactTargetQueueConfig) {
396
+ super(config.id);
397
+ }
398
+
399
+ protected parameters(): Record<string, unknown> {
400
+ return this.config.queue !== undefined
401
+ ? { QueueId: this.config.queue }
402
+ : { AgentId: this.config.agent };
403
+ }
404
+
405
+ protected transitions(): Transitions {
406
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
407
+ }
408
+ }
409
+
410
+ /** Takes no parameters. The queue comes from a preceding UpdateContactTargetQueue. */
411
+ export interface TransferContactToQueueConfig extends Wired {
412
+ onQueueAtCapacity: Target;
413
+ }
414
+
415
+ export class TransferContactToQueue extends Block {
416
+ readonly type = ActionType.TransferContactToQueue;
417
+
418
+ constructor(private readonly config: TransferContactToQueueConfig) {
419
+ super(config.id);
420
+ }
421
+
422
+ protected parameters(): Record<string, unknown> {
423
+ return {};
424
+ }
425
+
426
+ protected transitions(): Transitions {
427
+ const extra = EXTRA_ERRORS[ActionType.TransferContactToQueue] ?? [];
428
+ const errors: [string, Target][] = extra.map((e) => [e, this.config.onQueueAtCapacity]);
429
+ errors.push([NO_MATCHING_ERROR, this.config.onError]);
430
+ return wire(this.config.next, errors);
431
+ }
432
+ }
433
+
434
+ export interface UpdateContactAttributesConfig extends Wired {
435
+ attributes: Record<string, string>;
436
+ /** Defaults to Current. */
437
+ targetContact?: "Current" | "Related";
438
+ }
439
+
440
+ export class UpdateContactAttributes extends Block {
441
+ readonly type = ActionType.UpdateContactAttributes;
442
+
443
+ constructor(private readonly config: UpdateContactAttributesConfig) {
444
+ super(config.id);
445
+ }
446
+
447
+ protected parameters(): Record<string, unknown> {
448
+ return {
449
+ Attributes: this.config.attributes,
450
+ TargetContact: this.config.targetContact ?? "Current",
451
+ };
452
+ }
453
+
454
+ protected transitions(): Transitions {
455
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
456
+ }
457
+ }
458
+
459
+ export interface UpdateContactRecordingBehaviorConfig extends Wired {
460
+ recordedParticipants: ("Agent" | "Customer")[];
461
+ screenRecordedParticipants?: "Agent"[];
462
+ ivrRecordingBehavior?: "Enabled" | "Disabled";
463
+ }
464
+
465
+ export class UpdateContactRecordingBehavior extends Block {
466
+ readonly type = ActionType.UpdateContactRecordingBehavior;
467
+
468
+ constructor(private readonly config: UpdateContactRecordingBehaviorConfig) {
469
+ super(config.id);
470
+ }
471
+
472
+ protected parameters(): Record<string, unknown> {
473
+ const behavior: Record<string, unknown> = {
474
+ RecordedParticipants: this.config.recordedParticipants,
475
+ };
476
+ if (this.config.screenRecordedParticipants !== undefined) {
477
+ behavior.ScreenRecordedParticipants = this.config.screenRecordedParticipants;
478
+ }
479
+ if (this.config.ivrRecordingBehavior !== undefined) {
480
+ behavior.IVRRecordingBehavior = this.config.ivrRecordingBehavior;
481
+ }
482
+ return { RecordingBehavior: behavior };
483
+ }
484
+
485
+ protected transitions(): Transitions {
486
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
487
+ }
488
+ }
489
+
490
+ export interface InvokeFlowModuleConfig extends Wired {
491
+ module: Ref<"module"> | JsonPath;
492
+ }
493
+
494
+ export class InvokeFlowModule extends Block {
495
+ readonly type = ActionType.InvokeFlowModule;
496
+
497
+ constructor(private readonly config: InvokeFlowModuleConfig) {
498
+ super(config.id);
499
+ }
500
+
501
+ protected parameters(): Record<string, unknown> {
502
+ return { FlowModuleId: this.config.module };
503
+ }
504
+
505
+ protected transitions(): Transitions {
506
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
507
+ }
508
+ }
509
+
510
+ /** Terminal. Only legal inside a module. */
511
+ export class EndFlowModuleExecution extends Block {
512
+ readonly type = ActionType.EndFlowModuleExecution;
513
+
514
+ constructor(config: { id: string }) {
515
+ super(config.id);
516
+ }
517
+
518
+ protected parameters(): Record<string, unknown> {
519
+ return {};
520
+ }
521
+
522
+ protected transitions(): Transitions {
523
+ return {};
524
+ }
525
+ }
526
+
527
+ // ---------------------------------------------------------------------------
528
+ // Interactions
529
+ // ---------------------------------------------------------------------------
530
+
531
+ export interface InvokeLambdaFunctionConfig extends Wired {
532
+ lambda: Ref<"lambda"> | JsonPath;
533
+ /** Static integer, 1 to 8 inclusive. Connect rejects anything else. */
534
+ timeoutSeconds: number;
535
+ invocationType?: "SYNCHRONOUS" | "ASYNCHRONOUS";
536
+ attributes?: Record<string, string>;
537
+ responseType?: "STRING_MAP" | "JSON";
538
+ }
539
+
540
+ export class InvokeLambdaFunction extends Block {
541
+ readonly type = ActionType.InvokeLambdaFunction;
542
+
543
+ constructor(private readonly config: InvokeLambdaFunctionConfig) {
544
+ super(config.id);
545
+ const t = config.timeoutSeconds;
546
+ if (!Number.isInteger(t) || t < LAMBDA_TIMEOUT_MIN || t > LAMBDA_TIMEOUT_MAX) {
547
+ throw new Error(
548
+ `InvokeLambdaFunction "${config.id}" timeoutSeconds must be an integer between ${LAMBDA_TIMEOUT_MIN} and ${LAMBDA_TIMEOUT_MAX}, got ${t}.`,
549
+ );
550
+ }
551
+ }
552
+
553
+ protected parameters(): Record<string, unknown> {
554
+ const p: Record<string, unknown> = {
555
+ LambdaFunctionARN: this.config.lambda,
556
+ InvocationTimeLimitSeconds: this.config.timeoutSeconds,
557
+ InvocationType: this.config.invocationType ?? "SYNCHRONOUS",
558
+ };
559
+ if (this.config.attributes !== undefined) p.LambdaInvocationAttributes = this.config.attributes;
560
+ if (this.config.responseType !== undefined) {
561
+ p.ResponseValidation = { ResponseType: this.config.responseType };
562
+ }
563
+ return p;
564
+ }
565
+
566
+ protected transitions(): Transitions {
567
+ return wire(this.config.next, [[NO_MATCHING_ERROR, this.config.onError]]);
568
+ }
569
+ }
570
+
571
+ // ---------------------------------------------------------------------------
572
+ // Passthrough
573
+ // ---------------------------------------------------------------------------
574
+
575
+ /**
576
+ * Any Action the builder does not model. Preserved verbatim through synth,
577
+ * codegen, the studio, and both emitters. This is what keeps a small modeled
578
+ * set survivable: 49 action types are documented and the builder models 14.
579
+ */
580
+ export interface GenericBlockConfig {
581
+ id: string;
582
+ type: string;
583
+ parameters?: Record<string, unknown>;
584
+ next?: Target;
585
+ errors?: { errorType: string; target: Target }[];
586
+ conditions?: ConditionTransitionInput[];
587
+ }
588
+
589
+ export class GenericBlock extends Block {
590
+ readonly type: string;
591
+
592
+ constructor(private readonly config: GenericBlockConfig) {
593
+ super(config.id);
594
+ this.type = config.type;
595
+ }
596
+
597
+ protected parameters(): Record<string, unknown> {
598
+ return this.config.parameters ?? {};
599
+ }
600
+
601
+ protected transitions(): Transitions {
602
+ // A generic block with nothing wired is terminal, matching Connect's
603
+ // representation of terminal actions as an empty Transitions object.
604
+ if (
605
+ this.config.next === undefined &&
606
+ (this.config.errors ?? []).length === 0 &&
607
+ (this.config.conditions ?? []).length === 0
608
+ ) {
609
+ return {};
610
+ }
611
+ return wire(
612
+ this.config.next,
613
+ (this.config.errors ?? []).map((e) => [e.errorType, e.target] as [string, Target]),
614
+ this.config.conditions ?? [],
615
+ );
616
+ }
617
+ }
618
+
619
+ export type { Condition };