@teleporthq/teleport-plugin-next-workflows 0.45.4 → 0.45.6

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 (51) hide show
  1. package/__tests__/account-delete-password-check.test.ts +39 -0
  2. package/__tests__/analytics-outcome-visit-ids.test.ts +583 -0
  3. package/__tests__/automations-bus.test.ts +354 -0
  4. package/__tests__/public-step-guards.test.ts +26 -0
  5. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  6. package/dist/cjs/account-delete-route-generator.js +17 -8
  7. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  8. package/dist/cjs/api-route-generator.d.ts +2 -0
  9. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  10. package/dist/cjs/api-route-generator.js +43 -1
  11. package/dist/cjs/api-route-generator.js.map +1 -1
  12. package/dist/cjs/automations/dispatcher-source.d.ts.map +1 -1
  13. package/dist/cjs/automations/dispatcher-source.js +13 -5
  14. package/dist/cjs/automations/dispatcher-source.js.map +1 -1
  15. package/dist/cjs/executor-generator.d.ts.map +1 -1
  16. package/dist/cjs/executor-generator.js +4 -0
  17. package/dist/cjs/executor-generator.js.map +1 -1
  18. package/dist/cjs/public-step-guards.d.ts +2 -2
  19. package/dist/cjs/public-step-guards.d.ts.map +1 -1
  20. package/dist/cjs/public-step-guards.js +11 -8
  21. package/dist/cjs/public-step-guards.js.map +1 -1
  22. package/dist/cjs/publish-refusals.js +1 -1
  23. package/dist/cjs/publish-refusals.js.map +1 -1
  24. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  25. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  26. package/dist/esm/account-delete-route-generator.js +17 -8
  27. package/dist/esm/account-delete-route-generator.js.map +1 -1
  28. package/dist/esm/api-route-generator.d.ts +2 -0
  29. package/dist/esm/api-route-generator.d.ts.map +1 -1
  30. package/dist/esm/api-route-generator.js +44 -2
  31. package/dist/esm/api-route-generator.js.map +1 -1
  32. package/dist/esm/automations/dispatcher-source.d.ts.map +1 -1
  33. package/dist/esm/automations/dispatcher-source.js +13 -5
  34. package/dist/esm/automations/dispatcher-source.js.map +1 -1
  35. package/dist/esm/executor-generator.d.ts.map +1 -1
  36. package/dist/esm/executor-generator.js +4 -0
  37. package/dist/esm/executor-generator.js.map +1 -1
  38. package/dist/esm/public-step-guards.d.ts +2 -2
  39. package/dist/esm/public-step-guards.d.ts.map +1 -1
  40. package/dist/esm/public-step-guards.js +11 -8
  41. package/dist/esm/public-step-guards.js.map +1 -1
  42. package/dist/esm/publish-refusals.js +1 -1
  43. package/dist/esm/publish-refusals.js.map +1 -1
  44. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +2 -2
  46. package/src/account-delete-route-generator.ts +17 -8
  47. package/src/api-route-generator.ts +52 -2
  48. package/src/automations/dispatcher-source.ts +13 -5
  49. package/src/executor-generator.ts +4 -0
  50. package/src/public-step-guards.ts +14 -8
  51. package/src/publish-refusals.ts +1 -1
@@ -2887,6 +2887,360 @@ describe('in-app event bus — the mail automations send', () => {
2887
2887
  })
2888
2888
  })
2889
2889
 
2890
+ describe('in-app event bus — a run that sent nothing', () => {
2891
+ const mail = (id: string, to: unknown, label: string, stepNumber: number, extra: Row = {}) => ({
2892
+ id,
2893
+ type: 'email-resend',
2894
+ config: {
2895
+ apiKey: 'k',
2896
+ from: 'shop@example.com',
2897
+ to,
2898
+ subject: 'Order paid',
2899
+ body: '<p>Order paid</p>',
2900
+ ...extra,
2901
+ },
2902
+ stepNumber,
2903
+ label,
2904
+ })
2905
+ const gate = (stepNumber: number, left: unknown, rightValue: string) => ({
2906
+ id: 'gate',
2907
+ type: 'general-if-statement',
2908
+ config: { leftValue: left, operator: 'equals', rightValue },
2909
+ stepNumber,
2910
+ label: 'Check',
2911
+ })
2912
+ const OWNER = 'owner@shop.test'
2913
+ const orderNumber = ref('t', ['order', 'number'])
2914
+ const userEmail = ref('t', ['user', 'email'])
2915
+ const onPaid = (id: string, nodes: Row[], edges: Row[]) => ({
2916
+ id,
2917
+ name: id,
2918
+ trigger: trigger('order-paid'),
2919
+ nodes,
2920
+ edges,
2921
+ })
2922
+ const toGate = { id: 'e1', source: 't', target: 'gate' }
2923
+ const whenTrue = { id: 'e2', source: 'gate', target: 'send', sourceHandle: 'true' }
2924
+ const guarded = (id: string, rightValue: string) =>
2925
+ onPaid(
2926
+ id,
2927
+ [gate(1, orderNumber, rightValue), mail('send', OWNER, 'Send Email to Owner', 2)],
2928
+ [toGate, whenTrue]
2929
+ )
2930
+ const afterPost = { id: 'e0', source: 't', target: 'hold' }
2931
+ const holdToGate = { id: 'e1', source: 'hold', target: 'gate' }
2932
+ const parks = {
2933
+ id: 'wait',
2934
+ type: 'general-wait',
2935
+ config: { amount: 1, unit: 'days' },
2936
+ label: 'Wait a day',
2937
+ }
2938
+ const WORKFLOWS: Record<string, Row> = {
2939
+ 'wf-skipped': guarded('wf-skipped', 'NOT-THIS-ORDER'),
2940
+ 'wf-sent': guarded('wf-sent', 'ORD-1042'),
2941
+ 'wf-no-recipient': onPaid(
2942
+ 'wf-no-recipient',
2943
+ [mail('send', ref('t', ['order', 'nobody']), 'Send', 1)],
2944
+ [{ id: 'e1', source: 't', target: 'send' }]
2945
+ ),
2946
+ 'wf-failed-and-went-on': onPaid(
2947
+ 'wf-failed-and-went-on',
2948
+ [mail('send', ref('t', ['order', 'nobody']), 'Send', 1, { continueOnError: true })],
2949
+ [{ id: 'e1', source: 't', target: 'send' }]
2950
+ ),
2951
+ 'wf-one-of-two': onPaid(
2952
+ 'wf-one-of-two',
2953
+ [
2954
+ gate(1, orderNumber, 'NOT-THIS-ORDER'),
2955
+ mail('send', OWNER, 'Skipped', 2),
2956
+ mail('other', OWNER, 'Sent', 3),
2957
+ ],
2958
+ [toGate, whenTrue, { id: 'e3', source: 'gate', target: 'other', sourceHandle: 'false' }]
2959
+ ),
2960
+ 'wf-slow-skipped': onPaid(
2961
+ 'wf-slow-skipped',
2962
+ [
2963
+ post('hold', '/hold', 1),
2964
+ gate(2, orderNumber, 'NOT-THIS-ORDER'),
2965
+ mail('send', OWNER, 'Send Email to Owner', 3),
2966
+ ],
2967
+ [afterPost, holdToGate, whenTrue]
2968
+ ),
2969
+ 'wf-wait-then-skipped': {
2970
+ id: 'wf-wait-then-skipped',
2971
+ name: 'wf-wait-then-skipped',
2972
+ trigger: trigger('user-signed-up'),
2973
+ nodes: [
2974
+ { ...parks, stepNumber: 1 },
2975
+ post('hold', '/hold', 2),
2976
+ gate(3, userEmail, 'nobody@example.com'),
2977
+ mail('send', OWNER, 'Send Email to Owner', 4),
2978
+ ],
2979
+ edges: [
2980
+ { id: 'e0', source: 't', target: 'wait' },
2981
+ { id: 'e1', source: 'wait', target: 'hold' },
2982
+ holdToGate,
2983
+ whenTrue,
2984
+ ],
2985
+ },
2986
+ 'wf-mail-then-wait': {
2987
+ id: 'wf-mail-then-wait',
2988
+ name: 'wf-mail-then-wait',
2989
+ trigger: trigger('user-signed-up'),
2990
+ nodes: [
2991
+ mail('send', OWNER, 'Welcome', 1),
2992
+ { ...parks, stepNumber: 2 },
2993
+ post('after', '/after', 3),
2994
+ ],
2995
+ edges: [
2996
+ { id: 'e0', source: 't', target: 'send' },
2997
+ { id: 'e1', source: 'send', target: 'wait' },
2998
+ { id: 'e2', source: 'wait', target: 'after' },
2999
+ ],
3000
+ },
3001
+ }
3002
+ const NOTE = JSON.stringify({ sentNothing: ['Send Email to Owner'] })
3003
+ let files: Map<string, string>
3004
+
3005
+ beforeAll(async () => {
3006
+ files = sources(await generate(WORKFLOWS))
3007
+ })
3008
+
3009
+ const paidOrder = (workflowId: string) => {
3010
+ const db = new FakeDatabase()
3011
+ seedOrderPaid(db)
3012
+ db.automations = [automationRow(workflowId, 'order-paid')]
3013
+ return db
3014
+ }
3015
+ const signup = (workflowId: string) => {
3016
+ const db = new FakeDatabase()
3017
+ db.users.push({ id: 'user-1', email: 'noah@example.com', name: 'Noah', created_at: ago(1000) })
3018
+ db.automations = [automationRow(workflowId, 'user-signed-up')]
3019
+ db.events.push(
3020
+ eventRow({
3021
+ id: 'evt-signup',
3022
+ event_type: 'user-signed-up',
3023
+ entity_type: 'user',
3024
+ entity_id: 'user-1',
3025
+ actor: 'noah@example.com',
3026
+ actor_type: 'customer',
3027
+ })
3028
+ )
3029
+ return db
3030
+ }
3031
+ const dispatch = (db: FakeDatabase) =>
3032
+ withEcho(() =>
3033
+ silenced(() => (createApp(files, db)('lib/automations/event-bus.js') as Bus).dispatch({}))
3034
+ )
3035
+ /** A dispatch waiting 20 ms for a handler held at its first fetch, released afterwards. */
3036
+ const dispatchHeldOpen = async (db: FakeDatabase) => {
3037
+ const held: Array<() => void> = []
3038
+ const scope = globalThis as Record<string, unknown>
3039
+ const previousFetch = scope.fetch
3040
+ scope.fetch = () =>
3041
+ new Promise((resolve) => {
3042
+ held.push(() =>
3043
+ resolve({
3044
+ ok: true,
3045
+ status: 200,
3046
+ headers: new Map([['content-type', 'application/json']]),
3047
+ json: async () => ({}),
3048
+ text: async () => '{}',
3049
+ })
3050
+ )
3051
+ })
3052
+ try {
3053
+ await withEnv(ENV, () =>
3054
+ silenced(async () => {
3055
+ await (createApp(files, db)('lib/automations/event-bus.js') as Bus).dispatch({
3056
+ handlerTimeoutMs: 20,
3057
+ })
3058
+ expect(held).toHaveLength(1)
3059
+ held[0]()
3060
+ await new Promise((resolve) => setTimeout(resolve, 10))
3061
+ })
3062
+ )
3063
+ } finally {
3064
+ scope.fetch = previousFetch
3065
+ }
3066
+ }
3067
+
3068
+ it('records the send steps a condition skipped, and the run stays delivered', async () => {
3069
+ const db = paidOrder('wf-skipped')
3070
+
3071
+ const result = await dispatch(db)
3072
+
3073
+ expect(result).toEqual({ claimed: 1, delivered: 1, failed: 0, resumed: 0 })
3074
+ expect(db.mailSent).toEqual([])
3075
+ expect(db.delivery('wf-skipped')).toMatchObject({ status: 'delivered', error: NOTE })
3076
+ expect(db.events[0]).toMatchObject({ status: 'delivered', attempts: 1 })
3077
+ })
3078
+
3079
+ it('records nothing when a send step sent, or when the automation has none', async () => {
3080
+ const sent = paidOrder('wf-sent')
3081
+ await dispatch(sent)
3082
+ expect(sent.mailSent).toHaveLength(1)
3083
+ expect(sent.delivery('wf-sent')).toMatchObject({ status: 'delivered', error: null })
3084
+
3085
+ const oneOfTwo = paidOrder('wf-one-of-two')
3086
+ await dispatch(oneOfTwo)
3087
+ expect(oneOfTwo.mailSent).toHaveLength(1)
3088
+ expect(oneOfTwo.delivery('wf-one-of-two')).toMatchObject({ status: 'delivered', error: null })
3089
+ })
3090
+
3091
+ it('counts a send that failed and went on as unsent', async () => {
3092
+ const db = paidOrder('wf-failed-and-went-on')
3093
+ await dispatch(db)
3094
+ expect(db.delivery('wf-failed-and-went-on')).toMatchObject({
3095
+ status: 'delivered',
3096
+ error: JSON.stringify({ sentNothing: ['Send'] }),
3097
+ })
3098
+ })
3099
+
3100
+ it('leaves a send that stops the run failing as before, with its own error', async () => {
3101
+ const db = paidOrder('wf-no-recipient')
3102
+ const result = await dispatch(db)
3103
+ expect(result).toEqual({ claimed: 1, delivered: 0, failed: 1, resumed: 0 })
3104
+ expect(db.mailSent).toEqual([])
3105
+ expect(db.delivery('wf-no-recipient')).toMatchObject({
3106
+ status: 'failed',
3107
+ error: 'No recipient',
3108
+ })
3109
+ })
3110
+
3111
+ it('records it for a run that ends after the dispatcher stopped waiting', async () => {
3112
+ const db = paidOrder('wf-slow-skipped')
3113
+ await dispatchHeldOpen(db)
3114
+ expect(db.delivery('wf-slow-skipped')).toMatchObject({ status: 'delivered', error: NOTE })
3115
+ })
3116
+
3117
+ it('records it for a run resumed after its Wait', async () => {
3118
+ const db = signup('wf-wait-then-skipped')
3119
+ await dispatch(db)
3120
+ expect(db.delivery('wf-wait-then-skipped')).toMatchObject({ status: 'waiting', error: null })
3121
+
3122
+ db.waits[0].run_at = ago(1000)
3123
+ await dispatch(db)
3124
+
3125
+ expect(db.delivery('wf-wait-then-skipped')).toMatchObject({
3126
+ status: 'delivered',
3127
+ error: NOTE,
3128
+ })
3129
+ })
3130
+
3131
+ it('records it for a resume that ends after the dispatcher stopped waiting', async () => {
3132
+ const db = signup('wf-wait-then-skipped')
3133
+ await dispatch(db)
3134
+ db.waits[0].run_at = ago(1000)
3135
+ await dispatchHeldOpen(db)
3136
+ expect(db.delivery('wf-wait-then-skipped')).toMatchObject({
3137
+ status: 'delivered',
3138
+ error: NOTE,
3139
+ })
3140
+ })
3141
+
3142
+ it('counts a mail sent before the Wait when the run resumes after it', async () => {
3143
+ const db = signup('wf-mail-then-wait')
3144
+ await dispatch(db)
3145
+ db.waits[0].run_at = ago(1000)
3146
+ await dispatch(db)
3147
+ expect(db.mailSent).toHaveLength(1)
3148
+ expect(db.delivery('wf-mail-then-wait')).toMatchObject({ status: 'delivered', error: null })
3149
+ })
3150
+
3151
+ it('records it for a test event, whose row stays a test', async () => {
3152
+ const db = paidOrder('wf-skipped')
3153
+ db.events = []
3154
+
3155
+ const result = await withEcho(() =>
3156
+ silenced(() => {
3157
+ const { runTest } = createApp(files, db)('lib/automations/dispatcher.js') as {
3158
+ runTest(input: Row, options: Row): Promise<Totals>
3159
+ }
3160
+ return runTest(
3161
+ {
3162
+ workflowId: 'wf-skipped',
3163
+ event: { order: { number: 'ORD-TEST', customer: { email: 'test@example.com' } } },
3164
+ },
3165
+ {}
3166
+ )
3167
+ })
3168
+ )
3169
+
3170
+ expect(result).toEqual({ claimed: 1, delivered: 1, failed: 0, resumed: 0 })
3171
+ expect(db.delivery('wf-skipped')).toMatchObject({ status: 'test', error: NOTE })
3172
+ })
3173
+
3174
+ describe('which steps a run can report', () => {
3175
+ const sendStepsOf = async (workflow: Row): Promise<Row[]> => {
3176
+ const { generateHandlerFunction } = await import('../src/api-route-generator')
3177
+ const source = generateHandlerFunction(JSON.parse(JSON.stringify(workflow)))
3178
+ return JSON.parse(/var SEND_STEPS = (.*);/.exec(source)![1])
3179
+ }
3180
+ const sms = {
3181
+ id: 'text',
3182
+ type: 'sms-twilio',
3183
+ config: { to: '+15550142', message: 'Paid' },
3184
+ stepNumber: 3,
3185
+ label: 'Text the owner',
3186
+ }
3187
+ const email = mail('send', OWNER, 'Send Email to Owner', 2)
3188
+ const steps = [
3189
+ { id: 'send', label: 'Send Email to Owner' },
3190
+ { id: 'text', label: 'Text the owner' },
3191
+ ]
3192
+
3193
+ it('lists the email and SMS steps, and nothing else', async () => {
3194
+ expect(await sendStepsOf(onPaid('w', [post('p', '/p', 1), email, sms], []))).toEqual(steps)
3195
+ expect(await sendStepsOf(onPaid('w', [post('p', '/p', 1)], []))).toEqual([])
3196
+ })
3197
+
3198
+ it('leaves out the steps of the error branch, which a run that ends well never reaches', async () => {
3199
+ const onError = mail('alarm', OWNER, 'Tell me it broke', 4)
3200
+ const workflow = {
3201
+ ...onPaid(
3202
+ 'w',
3203
+ [
3204
+ email,
3205
+ { id: 'err', type: 'event-workflow-error', config: {}, stepNumber: 3, label: 'Error' },
3206
+ onError,
3207
+ ],
3208
+ [{ id: 'e1', source: 'err', target: 'alarm' }]
3209
+ ),
3210
+ errorHandler: { nodeId: 'err' },
3211
+ }
3212
+ expect(await sendStepsOf(workflow)).toEqual([{ id: 'send', label: 'Send Email to Owner' }])
3213
+ })
3214
+
3215
+ it('reports nothing when a step it cannot see may have sent', async () => {
3216
+ const customNode = {
3217
+ id: 'custom',
3218
+ type: 'general-custom-node',
3219
+ config: {},
3220
+ stepNumber: 1,
3221
+ label: 'Custom',
3222
+ }
3223
+ const parallel = (config: Row) => ({
3224
+ id: 'both',
3225
+ type: 'general-parallel',
3226
+ config,
3227
+ stepNumber: 1,
3228
+ label: 'Both',
3229
+ })
3230
+ expect(await sendStepsOf(onPaid('w', [customNode, email], []))).toEqual([])
3231
+ expect(await sendStepsOf(onPaid('w', [parallel({ waitForAll: false }), email], []))).toEqual(
3232
+ []
3233
+ )
3234
+ expect(await sendStepsOf(onPaid('w', [parallel({ waitForAll: true }), email], []))).toEqual([
3235
+ { id: 'send', label: 'Send Email to Owner' },
3236
+ ])
3237
+ expect(await sendStepsOf(onPaid('w', [parallel({}), email], []))).toEqual([
3238
+ { id: 'send', label: 'Send Email to Owner' },
3239
+ ])
3240
+ })
3241
+ })
3242
+ })
3243
+
2890
3244
  describe('in-app event bus — emitting and the dispatch route', () => {
2891
3245
  let files: Map<string, string>
2892
3246
 
@@ -3,6 +3,7 @@ import adminFileWorkflows from './fixtures/admin-file-workflows.json'
3
3
  import {
4
4
  PUBLIC_STEP_REFUSAL_CODE,
5
5
  PublicStepRefusalError,
6
+ assertNoPublicRestrictedSteps,
6
7
  findPublicRestrictedSteps,
7
8
  } from '../src/public-step-guards'
8
9
 
@@ -234,6 +235,31 @@ describe('restricted steps on a public route', () => {
234
235
  expect(error).toBeInstanceOf(PublicStepRefusalError)
235
236
  })
236
237
 
238
+ it('on a site without sign-in, says to enable authentication before any role can be asked', () => {
239
+ const workflows = {
240
+ workflows: { w1: pageWorkflow([node('n1', 'file-storage-delete', 'Delete Upload')]) },
241
+ }
242
+ const refusedWith = (authentication: unknown): string => {
243
+ try {
244
+ assertNoPublicRestrictedSteps(workflows, authentication as never)
245
+ } catch (error) {
246
+ return shownSentence(error as Error)
247
+ }
248
+ throw new Error('not refused')
249
+ }
250
+
251
+ for (const withoutSignIn of [refusedWith(null), refusedWith({ enabled: false, roles: [] })]) {
252
+ expect(withoutSignIn).toContain('"Delete Upload"')
253
+ expect(withoutSignIn).toContain('has no sign-in')
254
+ expect(withoutSignIn).toContain('enable authentication')
255
+ expect(withoutSignIn).not.toContain('let only the role')
256
+ expect(withoutSignIn.length).toBeLessThanOrEqual(400)
257
+ }
258
+ const withSignIn = refusedWith({ enabled: true, roles: ['admin', 'user'] })
259
+ expect(withSignIn).toContain('let only the role "admin" start it')
260
+ expect(withSignIn).not.toContain('has no sign-in')
261
+ })
262
+
237
263
  it('stays one short sentence for many steps', () => {
238
264
  const nodes = Array.from({ length: 12 }, (_, i) =>
239
265
  node(
@@ -1 +1 @@
1
- {"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAcxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAG5B,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAwbpF,CAAA"}
1
+ {"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAcxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAG5B,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAicpF,CAAA"}
@@ -275,19 +275,28 @@ function readRequestPassword(req) {
275
275
  return String(body.password);
276
276
  }
277
277
 
278
- // The sign-in check (utils/auth/hash-password.js verifyPassword): bcrypt over
279
- // the SHA-256 of the password and AUTH_PEPPER. An account without a stored
280
- // password (a social sign-in) matches no password.
278
+ // The sign-in check (utils/auth/hash-password.js checkPassword): bcrypt over
279
+ // the SHA-256 of the password and a pepper, the project's own
280
+ // (AUTH_PEPPER_SECRET) first, then the one every earlier hash used
281
+ // (AUTH_PEPPER). An account without a stored password (a social sign-in)
282
+ // matches no password.
281
283
  function passwordMatches(password, storedHash) {
282
284
  if (!storedHash || password.length > 1000) { return false; }
283
285
  var bcrypt = require('bcryptjs');
284
286
  var crypto = require('crypto');
285
- var prepared = crypto.createHash('sha256').update(password + (process.env.AUTH_PEPPER || '')).digest('base64');
286
- try {
287
- return bcrypt.compareSync(prepared, String(storedHash));
288
- } catch (e) {
289
- return false;
287
+ var secret = process.env.AUTH_PEPPER_SECRET;
288
+ var peppers = typeof secret === 'string' && secret && secret.indexOf('teleporthq.secrets.') !== 0
289
+ ? [secret, process.env.AUTH_PEPPER || '']
290
+ : [process.env.AUTH_PEPPER || ''];
291
+ for (var i = 0; i < peppers.length; i++) {
292
+ var prepared = crypto.createHash('sha256').update(password + peppers[i]).digest('base64');
293
+ try {
294
+ if (bcrypt.compareSync(prepared, String(storedHash))) { return true; }
295
+ } catch (e) {
296
+ return false;
297
+ }
290
298
  }
299
+ return false;
291
300
  }
292
301
 
293
302
  ${(0, transactional_email_code_1.generateFillTemplateFn)()}
@@ -1 +1 @@
1
- {"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"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"}
@@ -65,6 +65,8 @@ export declare const generateWebhookWorkflowAPIRoute: (workflow: UIDLWorkflow, c
65
65
  * nothing and resolves `{ waiting: true, wouldWait }`; a Wait with no step
66
66
  * after it stores nothing and the run ends there). A run that fails rejects,
67
67
  * after the error-handler branch ran, so the dispatcher records the failure.
68
+ * A run that ends with its email and SMS steps all unsent resolves with their
69
+ * names under `sentNothing`, so the History can say so instead of Done.
68
70
  */
69
71
  export declare const generateHandlerFunction: (workflow: UIDLWorkflow, customNodes?: Record<string, UIDLCustomWorkflowNode>) => string;
70
72
  export declare const getWebhookRouteFileName: (workflow: UIDLWorkflow) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAkEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAyXF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAycF,CAAA;AA6ID;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aA4D5B,CAAA;AAEF,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAqBD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MA0HF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MA4BF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AAsTD,uHAAuH;AACvH,eAAO,MAAM,uBAAuB,kBAAmB,iBAAiB,KAAG,OAES,CAAA;AAEpF,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MAkOF,CAAA;AAuCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,aACxB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MA8JF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,cAC1B,aAAa,GAAG,SAAS,cACxB,MAAM,KACjB,MAAM,GAAG,IAYX,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
1
+ {"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAuEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAyXF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAycF,CAAA;AA6ID;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aA4D5B,CAAA;AAEF,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAqBD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MA0HF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MA4BF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AAsTD,uHAAuH;AACvH,eAAO,MAAM,uBAAuB,kBAAmB,iBAAiB,KAAG,OAES,CAAA;AAEpF,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MAkOF,CAAA;AAkED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,uBAAuB,aACxB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MA8KF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,cAC1B,aAAa,GAAG,SAAS,cACxB,MAAM,KACjB,MAAM,GAAG,IAYX,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
@@ -8,6 +8,7 @@ const nodes_1 = require("./nodes");
8
8
  const types_1 = require("./nodes/types");
9
9
  const general_wait_1 = require("./nodes/general/general-wait");
10
10
  const visitor_request_1 = require("./nodes/general/visitor-request");
11
+ const message_trust_1 = require("./nodes/email/message-trust");
11
12
  const outbound_message_guard_1 = require("./nodes/email/outbound-message-guard");
12
13
  const ai_provider_utils_1 = require("./nodes/ai/ai-provider-utils");
13
14
  const automations_scope_1 = require("./automations/automations-scope");
@@ -1916,6 +1917,29 @@ const generateErrorHandlerRunBlock = (failureLabel) => ` if (ERROR_HANDLER_NO
1916
1917
  console.error('${failureLabel}', innerErr);
1917
1918
  }
1918
1919
  }`;
1920
+ /**
1921
+ * The email and SMS steps a run of this automation can report as not having
1922
+ * sent. None when it could not tell: a custom node may send by itself, and a
1923
+ * parallel branch the run does not wait for has no result when the run ends.
1924
+ * Steps on the error branch do not count: a run that ends well never reaches them.
1925
+ */
1926
+ const reportableSendSteps = (workflow, errorHandlerNodes) => {
1927
+ const nodes = workflow.nodes || [];
1928
+ const couldNotTell = nodes.some((node) => {
1929
+ var _a;
1930
+ return node.type === 'general-custom-node' ||
1931
+ (node.type === 'general-parallel' &&
1932
+ ((_a = node.config) === null || _a === void 0 ? void 0 : _a.waitForAll) !== undefined &&
1933
+ node.config.waitForAll !== true);
1934
+ });
1935
+ if (couldNotTell) {
1936
+ return [];
1937
+ }
1938
+ const errorBranch = new Set((errorHandlerNodes || []).map((node) => node.id));
1939
+ return nodes
1940
+ .filter((node) => (0, visitor_request_1.isOutboundMessageNodeType)(node.type) && !errorBranch.has(node.id))
1941
+ .map((node) => ({ id: node.id, label: (0, message_trust_1.stepNameOf)(node) }));
1942
+ };
1919
1943
  /**
1920
1944
  * An automation's handler module (`lib/automations/handlers/<file>.js`, see
1921
1945
  * automations/handler-module-generator.ts): the webhook route's node loop as
@@ -1935,6 +1959,8 @@ const generateErrorHandlerRunBlock = (failureLabel) => ` if (ERROR_HANDLER_NO
1935
1959
  * nothing and resolves `{ waiting: true, wouldWait }`; a Wait with no step
1936
1960
  * after it stores nothing and the run ends there). A run that fails rejects,
1937
1961
  * after the error-handler branch ran, so the dispatcher records the failure.
1962
+ * A run that ends with its email and SMS steps all unsent resolves with their
1963
+ * names under `sentNothing`, so the History can say so instead of Done.
1938
1964
  */
1939
1965
  const generateHandlerFunction = (workflow, customNodes) => {
1940
1966
  var _a;
@@ -1992,6 +2018,8 @@ const WORKFLOW_CONFIG = ${workflowConfig};
1992
2018
 
1993
2019
  var ERROR_HANDLER_NODES = ${errorHandlerNodesJson};
1994
2020
 
2021
+ var SEND_STEPS = ${JSON.stringify(reportableSendSteps(workflow, errorHandlerNodes))};
2022
+
1995
2023
  const nodeHandlers = {
1996
2024
  ${nodeHandlersEntries}
1997
2025
  };
@@ -2033,6 +2061,17 @@ ${executionLoop}
2033
2061
  return { waiting: false };
2034
2062
  }
2035
2063
 
2064
+ // The names of the send steps when the run has some and none of them sent: a
2065
+ // step a condition skipped has no result, one that failed and went on says so.
2066
+ function unsentSendSteps(context) {
2067
+ if (SEND_STEPS.length === 0) return null;
2068
+ var sent = SEND_STEPS.some(function(step) {
2069
+ var result = context[step.id];
2070
+ return !!result && !utils.isFatalNodeResult(result);
2071
+ });
2072
+ return sent ? null : SEND_STEPS.map(function(step) { return step.label; });
2073
+ }
2074
+
2036
2075
  async function run(event, context, resumeFrom) {
2037
2076
  context = context && typeof context === 'object' ? context : {};
2038
2077
  // Shared fire-and-forget queue — drained before the run settles.
@@ -2065,13 +2104,16 @@ async function run(event, context, resumeFrom) {
2065
2104
  sourceRef: WORKFLOW_ID,
2066
2105
  orderId: trigger.order && trigger.order.id ? String(trigger.order.id) : null,
2067
2106
  };
2107
+ var outcome;
2068
2108
  try {
2069
- return await runNodes(context, resumeFrom || null);
2109
+ outcome = await runNodes(context, resumeFrom || null);
2070
2110
  } catch (error) {
2071
2111
  ${generateErrorHandlerRunBlock('Automation error handler failed:')}
2072
2112
  await utils.settlePendingNodePromises(context);
2073
2113
  throw error;
2074
2114
  }
2115
+ var unsent = outcome.waiting ? null : unsentSendSteps(context);
2116
+ return unsent ? Object.assign({}, outcome, { sentNothing: unsent }) : outcome;
2075
2117
  }
2076
2118
 
2077
2119
  module.exports.run = run;