@whitewall/blip-sdk 0.0.192 → 0.0.193

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 (36) hide show
  1. package/dist/cjs/index.js +1 -0
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/flow.js +34 -1
  4. package/dist/cjs/types/flow.js.map +1 -1
  5. package/dist/cjs/utils/builder.js +9 -28
  6. package/dist/cjs/utils/builder.js.map +1 -1
  7. package/dist/cjs/utils/flow-rules.js +304 -0
  8. package/dist/cjs/utils/flow-rules.js.map +1 -0
  9. package/dist/cjs/utils/minifier.js +42 -92
  10. package/dist/cjs/utils/minifier.js.map +1 -1
  11. package/dist/esm/index.js +1 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/types/flow.js +29 -0
  14. package/dist/esm/types/flow.js.map +1 -1
  15. package/dist/esm/utils/builder.js +9 -27
  16. package/dist/esm/utils/builder.js.map +1 -1
  17. package/dist/esm/utils/flow-rules.js +297 -0
  18. package/dist/esm/utils/flow-rules.js.map +1 -0
  19. package/dist/esm/utils/minifier.js +40 -90
  20. package/dist/esm/utils/minifier.js.map +1 -1
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/dist/types/types/flow.d.ts +33 -0
  24. package/dist/types/types/flow.d.ts.map +1 -1
  25. package/dist/types/utils/builder.d.ts +16 -25
  26. package/dist/types/utils/builder.d.ts.map +1 -1
  27. package/dist/types/utils/flow-rules.d.ts +50 -0
  28. package/dist/types/utils/flow-rules.d.ts.map +1 -0
  29. package/dist/types/utils/minifier.d.ts +294 -2
  30. package/dist/types/utils/minifier.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/index.ts +1 -0
  33. package/src/types/flow.ts +40 -0
  34. package/src/utils/builder.ts +29 -62
  35. package/src/utils/flow-rules.ts +453 -0
  36. package/src/utils/minifier.ts +46 -95
@@ -11,17 +11,19 @@ import {
11
11
  actionTypeSchema,
12
12
  baseActionSchema,
13
13
  type CONTENT_TYPES,
14
+ CUSTOM_ACTION_LISTS,
14
15
  conditionOutputSchema,
15
16
  defaultOutputSchema,
16
17
  type Flow,
17
18
  type InputState,
18
19
  inputStateSchema,
20
+ isChatStateSendMessage,
19
21
  type MIME_TYPES,
20
22
  ROOT_STATE_ID,
21
23
  type State,
22
24
  stateSchema,
23
25
  } from '../types/flow.ts'
24
- import { isChatStateSendMessage } from './builder.ts'
26
+ import { checkFlow } from './flow-rules.ts'
25
27
 
26
28
  /**
27
29
  * Everything `minifyFlow` removes, grouped by where it lives. Together these
@@ -35,19 +37,12 @@ const STATE_NOISE = [
35
37
  '$invalidCustomActions',
36
38
  '$invalid',
37
39
  'id',
38
- 'root',
39
40
  '$tags',
40
41
  '$inputSuggestions',
41
42
  'isAiGenerated',
42
43
  'deskStateVersion',
43
44
  ] as const
44
45
 
45
- const CUSTOM_ACTION_LISTS = [
46
- '$enteringCustomActions',
47
- '$leavingCustomActions',
48
- '$afterStateChangedActions',
49
- '$localCustomActions',
50
- ] as const
51
46
  const CUSTOM_ACTION_NOISE = ['$id', '$title', '$invalid', '$typeOfContent'] as const
52
47
 
53
48
  const CONTENT_ACTION_ITEM_NOISE = ['$invalid'] as const
@@ -144,32 +139,7 @@ function stripAngularKeys(target: unknown): void {
144
139
  }
145
140
  }
146
141
 
147
- const minifiedActionSchema = z.intersection(baseActionSchema.partial({ $id: true }), actionTypeSchema).check((ctx) => {
148
- // expandMinifiedFlow derives $typeOfContent from the settings mime type, so
149
- // a SendMessage without one cannot be pushed. Rejecting it here keeps
150
- // validation and push in agreement instead of failing halfway through.
151
- const action = ctx.value
152
- if (action.type === 'SendMessage' && !action.settings.type) {
153
- ctx.issues.push({
154
- code: 'custom',
155
- input: action,
156
- path: ['settings', 'type'],
157
- message: 'A SendMessage action requires a settings.type',
158
- })
159
- }
160
-
161
- // The runtime deserializes the raw content as this media type and refuses to
162
- // run the action without one, so a hand-written raw message that omits it
163
- // would be accepted here and then fail in the conversation.
164
- if (action.type === 'SendRawMessage' && !action.settings.type) {
165
- ctx.issues.push({
166
- code: 'custom',
167
- input: action,
168
- path: ['settings', 'type'],
169
- message: 'A SendRawMessage action requires a settings.type',
170
- })
171
- }
172
- })
142
+ const minifiedActionSchema = z.intersection(baseActionSchema.partial({ $id: true }), actionTypeSchema)
173
143
 
174
144
  const minifiedInputSchema = inputStateSchema.partial({ $cardContent: true, $invalid: true }).strict()
175
145
 
@@ -205,50 +175,22 @@ export const minifiedStateSchema = stateSchema
205
175
  .strict()
206
176
 
207
177
  /**
208
- * A push replaces the whole flow, so a dangling output and a title collision are
209
- * validation errors here instead of a broken canvas later.
178
+ * A push replaces the whole flow, so everything that decides whether the runtime
179
+ * loads it — the entry point, the outputs, the loops, every condition and every
180
+ * action — is a validation error here instead of a bot that does not start.
181
+ *
182
+ * The rules run from this one check, and never per state or per action: zod skips
183
+ * a check on a value that already has issues, so nesting them would report the
184
+ * innermost problem and hide the rest of the flow's.
210
185
  */
211
186
  export const minifiedFlowSchema = z.record(z.string(), minifiedStateSchema).check((ctx) => {
212
- const titles = new Map<string, string>()
213
-
214
- const checkOutput = (output: { stateId: string; typeOfStateId?: string }, path: Array<string | number>) => {
215
- // A variable output is only resolved at runtime.
216
- if (output.typeOfStateId === 'variable' || output.stateId in ctx.value) {
217
- return
218
- }
219
-
220
- ctx.issues.push({
221
- code: 'custom',
222
- input: output.stateId,
223
- path: [...path, 'stateId'],
224
- message: `Output points at '${output.stateId}', which is not a state in this flow`,
225
- })
226
- }
227
-
228
- for (const [stateId, state] of Object.entries(ctx.value)) {
229
- // The builder addresses a state by its title, so a duplicate hides one.
230
- const title = state.$title.toLowerCase()
231
- const duplicate = titles.get(title)
232
-
233
- if (duplicate) {
234
- ctx.issues.push({
235
- code: 'custom',
236
- input: state.$title,
237
- path: [stateId, '$title'],
238
- message: `Duplicate state title '${state.$title}', already used by '${duplicate}'`,
239
- })
240
- } else {
241
- titles.set(title, stateId)
242
- }
243
-
244
- checkOutput(state.$defaultOutput, [stateId, '$defaultOutput'])
245
-
246
- for (const [index, output] of state.$conditionOutputs.entries()) {
247
- checkOutput(output, [stateId, '$conditionOutputs', index])
248
- }
249
- }
187
+ checkFlow(ctx.value, (rule, path, message) => {
188
+ ctx.issues.push({ code: 'custom', input: ctx.value, path, message, params: { rule } })
189
+ })
250
190
  })
251
191
 
192
+ export type MinifiedAction = z.infer<typeof minifiedActionSchema>
193
+ export type MinifiedInput = z.infer<typeof minifiedInputSchema>
252
194
  export type MinifiedState = z.infer<typeof minifiedStateSchema>
253
195
  export type MinifiedFlow = z.infer<typeof minifiedFlowSchema>
254
196
 
@@ -271,10 +213,14 @@ export function minifyFlow(flow: Flow): MinifiedFlow {
271
213
  stripAngularKeys(minifiedState)
272
214
  stripKeys(minifiedState.$defaultOutput, DEFAULT_OUTPUT_NOISE)
273
215
 
274
- // Expansion writes `false` here for a state that has no plugin, so only a
275
- // real configuration is worth carrying.
216
+ // Only the state that is the entry point, and only a plugin that is really
217
+ // configured, are worth carrying: expansion writes `false` on all the others.
218
+ if (!minifiedState.root) {
219
+ delete minifiedState.root
220
+ }
221
+
276
222
  if (!minifiedState.$whiteWallIntegration) {
277
- stripKeys(minifiedState, ['$whiteWallIntegration'])
223
+ delete minifiedState.$whiteWallIntegration
278
224
  }
279
225
 
280
226
  for (const output of minifiedState.$conditionOutputs ?? []) {
@@ -307,6 +253,9 @@ export function minifyFlow(flow: Flow): MinifiedFlow {
307
253
 
308
254
  export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow): Flow {
309
255
  const flow: Flow = {}
256
+ // All or nothing: mixing the flow's own entry point with the builder's would
257
+ // leave two states marked as root, which the runtime refuses.
258
+ const declaresRoot = Object.values(minifiedFlow).some((state) => state.root)
310
259
 
311
260
  for (const [stateId, minifiedState] of Object.entries(minifiedFlow)) {
312
261
  try {
@@ -318,10 +267,16 @@ export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow)
318
267
  state.$invalidCustomActions = false
319
268
  state.$invalid = false
320
269
  state.id = stateId
321
- // When the builder already knows the state, its answer wins; a state
322
- // being pushed for the first time falls back to the id convention, so
323
- // a flow pushed without a stale one still gets an entry point.
324
- state.root = staleState ? staleState.root : stateId === ROOT_STATE_ID || undefined
270
+ // The entry point is carried by the flow, because `Flow.Validate`
271
+ // asserts on the flag and nothing else can express "this state, not the
272
+ // one named `onboarding`". A flow that predates it falls back to what
273
+ // the builder already knows, then to the id convention, so a first push
274
+ // still gets an entry point either way.
275
+ state.root = declaresRoot
276
+ ? Boolean(minifiedState.root)
277
+ : staleState
278
+ ? staleState.root
279
+ : stateId === ROOT_STATE_ID || undefined
325
280
  state.$tags = staleState?.$tags ?? []
326
281
  state.$inputSuggestions = staleState?.$inputSuggestions ?? []
327
282
  state.isAiGenerated = staleState?.isAiGenerated ?? false
@@ -330,7 +285,7 @@ export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow)
330
285
  // keeps a plugin configured before this was carried through.
331
286
  state.$whiteWallIntegration =
332
287
  minifiedState.$whiteWallIntegration ?? staleState?.$whiteWallIntegration ?? false
333
- state.deskStateVersion = staleState?.deskStateVersion ?? undefined
288
+ state.deskStateVersion = staleState?.deskStateVersion
334
289
  state.$defaultOutput.$invalid = false
335
290
 
336
291
  // Reuse the ids the builder already knows so the canvas keeps its
@@ -342,11 +297,11 @@ export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow)
342
297
 
343
298
  conditionOutput.$id = staleConditionOutput?.$id ?? crypto.randomUUID()
344
299
  conditionOutput.$invalid = false
345
- conditionOutput.$connId = staleConditionOutput?.$connId ?? undefined
346
- conditionOutput.$isDeskOutput = staleConditionOutput?.$isDeskOutput ?? undefined
347
- conditionOutput.$isDeskCustomOutput = staleConditionOutput?.$isDeskCustomOutput ?? undefined
348
- conditionOutput.$isDeskDefaultOutput = staleConditionOutput?.$isDeskDefaultOutput ?? undefined
349
- conditionOutput.$contentId = staleConditionOutput?.$contentId ?? undefined
300
+ conditionOutput.$connId = staleConditionOutput?.$connId
301
+ conditionOutput.$isDeskOutput = staleConditionOutput?.$isDeskOutput
302
+ conditionOutput.$isDeskCustomOutput = staleConditionOutput?.$isDeskCustomOutput
303
+ conditionOutput.$isDeskDefaultOutput = staleConditionOutput?.$isDeskDefaultOutput
304
+ conditionOutput.$contentId = staleConditionOutput?.$contentId
350
305
  }
351
306
 
352
307
  for (const actionType of CUSTOM_ACTION_LISTS) {
@@ -358,7 +313,7 @@ export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow)
358
313
  action.$id = staleAction?.$id ?? crypto.randomUUID()
359
314
  action.$invalid = false
360
315
  action.$title = staleAction?.$title ?? createActionTitle(action)
361
- action.$typeOfContent = staleAction?.$typeOfContent ?? undefined
316
+ action.$typeOfContent = staleAction?.$typeOfContent
362
317
  }
363
318
  }
364
319
 
@@ -416,13 +371,9 @@ export function expandMinifiedFlow(minifiedFlow: MinifiedFlow, staleFlow?: Flow)
416
371
  }
417
372
  }
418
373
 
419
- // Walk backwards so the chat states can be spliced in without shifting
420
- // the indexes still to be visited.
421
- for (let index = (state.$contentActions ?? []).length - 1; index >= 0; index--) {
422
- if (state.$contentActions[index].action) {
423
- state.$contentActions.splice(index, 0, createChatStateContentAction())
424
- }
425
- }
374
+ state.$contentActions = (state.$contentActions ?? []).flatMap((item) =>
375
+ item.action ? [createChatStateContentAction(), item] : [item],
376
+ )
426
377
 
427
378
  flow[stateId] = state
428
379
  } catch (err) {