@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teleporthq/teleport-plugin-next-workflows",
3
- "version": "0.45.4",
3
+ "version": "0.45.6",
4
4
  "description": "A plugin for workflow code generation in Next.js projects.",
5
5
  "author": "teleportHQ",
6
6
  "license": "MIT",
@@ -69,5 +69,5 @@
69
69
  "@teleporthq/teleport-shared": "^0.45.0",
70
70
  "@teleporthq/teleport-types": "^0.45.0"
71
71
  },
72
- "gitHead": "4fc86526c794a1304f0298809bf3530b4d380486"
72
+ "gitHead": "c31e265b69774868545e810e31a3c748f62a1729"
73
73
  }
@@ -335,19 +335,28 @@ function readRequestPassword(req) {
335
335
  return String(body.password);
336
336
  }
337
337
 
338
- // The sign-in check (utils/auth/hash-password.js verifyPassword): bcrypt over
339
- // the SHA-256 of the password and AUTH_PEPPER. An account without a stored
340
- // password (a social sign-in) matches no password.
338
+ // The sign-in check (utils/auth/hash-password.js checkPassword): bcrypt over
339
+ // the SHA-256 of the password and a pepper, the project's own
340
+ // (AUTH_PEPPER_SECRET) first, then the one every earlier hash used
341
+ // (AUTH_PEPPER). An account without a stored password (a social sign-in)
342
+ // matches no password.
341
343
  function passwordMatches(password, storedHash) {
342
344
  if (!storedHash || password.length > 1000) { return false; }
343
345
  var bcrypt = require('bcryptjs');
344
346
  var crypto = require('crypto');
345
- var prepared = crypto.createHash('sha256').update(password + (process.env.AUTH_PEPPER || '')).digest('base64');
346
- try {
347
- return bcrypt.compareSync(prepared, String(storedHash));
348
- } catch (e) {
349
- return false;
347
+ var secret = process.env.AUTH_PEPPER_SECRET;
348
+ var peppers = typeof secret === 'string' && secret && secret.indexOf('teleporthq.secrets.') !== 0
349
+ ? [secret, process.env.AUTH_PEPPER || '']
350
+ : [process.env.AUTH_PEPPER || ''];
351
+ for (var i = 0; i < peppers.length; i++) {
352
+ var prepared = crypto.createHash('sha256').update(password + peppers[i]).digest('base64');
353
+ try {
354
+ if (bcrypt.compareSync(prepared, String(storedHash))) { return true; }
355
+ } catch (e) {
356
+ return false;
357
+ }
350
358
  }
359
+ return false;
351
360
  }
352
361
 
353
362
  ${generateFillTemplateFn()}
@@ -14,7 +14,12 @@ import { isCostBearingNodeType } from './nodes/cost-bearing-node-types'
14
14
  import { nodeRegistry } from './nodes'
15
15
  import { NodeHandlerGenerator, ServerHandlerOptions, resolveHandlerEntryName } from './nodes/types'
16
16
  import { generalWait } from './nodes/general/general-wait'
17
- import { needsVisitorRequest, VISITOR_REQUEST_INJECTION } from './nodes/general/visitor-request'
17
+ import {
18
+ isOutboundMessageNodeType,
19
+ needsVisitorRequest,
20
+ VISITOR_REQUEST_INJECTION,
21
+ } from './nodes/general/visitor-request'
22
+ import { stepNameOf } from './nodes/email/message-trust'
18
23
  import {
19
24
  markVisitorBoundMessageNodes,
20
25
  markWebhookMessageNodes,
@@ -2180,6 +2185,33 @@ const generateErrorHandlerRunBlock = (
2180
2185
  }
2181
2186
  }`
2182
2187
 
2188
+ /**
2189
+ * The email and SMS steps a run of this automation can report as not having
2190
+ * sent. None when it could not tell: a custom node may send by itself, and a
2191
+ * parallel branch the run does not wait for has no result when the run ends.
2192
+ * Steps on the error branch do not count: a run that ends well never reaches them.
2193
+ */
2194
+ const reportableSendSteps = (
2195
+ workflow: UIDLWorkflow,
2196
+ errorHandlerNodes: Array<{ id: string }> | null
2197
+ ): Array<{ id: string; label: string }> => {
2198
+ const nodes = workflow.nodes || []
2199
+ const couldNotTell = nodes.some(
2200
+ (node) =>
2201
+ node.type === 'general-custom-node' ||
2202
+ (node.type === 'general-parallel' &&
2203
+ node.config?.waitForAll !== undefined &&
2204
+ node.config.waitForAll !== true)
2205
+ )
2206
+ if (couldNotTell) {
2207
+ return []
2208
+ }
2209
+ const errorBranch = new Set((errorHandlerNodes || []).map((node) => node.id))
2210
+ return nodes
2211
+ .filter((node) => isOutboundMessageNodeType(node.type) && !errorBranch.has(node.id))
2212
+ .map((node) => ({ id: node.id, label: stepNameOf(node) }))
2213
+ }
2214
+
2183
2215
  /**
2184
2216
  * An automation's handler module (`lib/automations/handlers/<file>.js`, see
2185
2217
  * automations/handler-module-generator.ts): the webhook route's node loop as
@@ -2199,6 +2231,8 @@ const generateErrorHandlerRunBlock = (
2199
2231
  * nothing and resolves `{ waiting: true, wouldWait }`; a Wait with no step
2200
2232
  * after it stores nothing and the run ends there). A run that fails rejects,
2201
2233
  * after the error-handler branch ran, so the dispatcher records the failure.
2234
+ * A run that ends with its email and SMS steps all unsent resolves with their
2235
+ * names under `sentNothing`, so the History can say so instead of Done.
2202
2236
  */
2203
2237
  export const generateHandlerFunction = (
2204
2238
  workflow: UIDLWorkflow,
@@ -2277,6 +2311,8 @@ const WORKFLOW_CONFIG = ${workflowConfig};
2277
2311
 
2278
2312
  var ERROR_HANDLER_NODES = ${errorHandlerNodesJson};
2279
2313
 
2314
+ var SEND_STEPS = ${JSON.stringify(reportableSendSteps(workflow, errorHandlerNodes))};
2315
+
2280
2316
  const nodeHandlers = {
2281
2317
  ${nodeHandlersEntries}
2282
2318
  };
@@ -2318,6 +2354,17 @@ ${executionLoop}
2318
2354
  return { waiting: false };
2319
2355
  }
2320
2356
 
2357
+ // The names of the send steps when the run has some and none of them sent: a
2358
+ // step a condition skipped has no result, one that failed and went on says so.
2359
+ function unsentSendSteps(context) {
2360
+ if (SEND_STEPS.length === 0) return null;
2361
+ var sent = SEND_STEPS.some(function(step) {
2362
+ var result = context[step.id];
2363
+ return !!result && !utils.isFatalNodeResult(result);
2364
+ });
2365
+ return sent ? null : SEND_STEPS.map(function(step) { return step.label; });
2366
+ }
2367
+
2321
2368
  async function run(event, context, resumeFrom) {
2322
2369
  context = context && typeof context === 'object' ? context : {};
2323
2370
  // Shared fire-and-forget queue — drained before the run settles.
@@ -2350,13 +2397,16 @@ async function run(event, context, resumeFrom) {
2350
2397
  sourceRef: WORKFLOW_ID,
2351
2398
  orderId: trigger.order && trigger.order.id ? String(trigger.order.id) : null,
2352
2399
  };
2400
+ var outcome;
2353
2401
  try {
2354
- return await runNodes(context, resumeFrom || null);
2402
+ outcome = await runNodes(context, resumeFrom || null);
2355
2403
  } catch (error) {
2356
2404
  ${generateErrorHandlerRunBlock('Automation error handler failed:')}
2357
2405
  await utils.settlePendingNodePromises(context);
2358
2406
  throw error;
2359
2407
  }
2408
+ var unsent = outcome.waiting ? null : unsentSendSteps(context);
2409
+ return unsent ? Object.assign({}, outcome, { sentNothing: unsent }) : outcome;
2360
2410
  }
2361
2411
 
2362
2412
  module.exports.run = run;
@@ -361,6 +361,14 @@ function runStatus(error, value) {
361
361
  return value && value.waiting === true ? 'waiting' : 'delivered';
362
362
  }
363
363
 
364
+ // What a run leaves in its delivery's error column: its error, or, for a run
365
+ // that finished, the send steps that did not send. Its status says nothing of
366
+ // them: such a run is delivered, and is neither retried nor counted as failed.
367
+ function runNote(error, value) {
368
+ if (error) return store.errorMessage(error);
369
+ return value && value.sentNothing ? JSON.stringify({ sentNothing: value.sentNothing }) : null;
370
+ }
371
+
364
372
  // Whether a guarded write found its row (the claim it names still held it).
365
373
  function wrote(result) {
366
374
  return !!result && result.rowCount > 0;
@@ -368,7 +376,7 @@ function wrote(result) {
368
376
 
369
377
  function lateDeliveryWriter(delivery, testRun, resumePoint) {
370
378
  return function (error, value) {
371
- var message = error ? store.errorMessage(error) : null;
379
+ var message = runNote(error, value);
372
380
  var status = testRun ? 'test' : runStatus(error, value);
373
381
  store.withClient(async function (client) {
374
382
  var finished = await client.query(FINISH_DELIVERY_SQL, [delivery.id, status, message, delivery.attempts]);
@@ -386,7 +394,7 @@ function lateWaitWriter(wait, resumedAt) {
386
394
  // Nothing is written once another dispatcher took the stale resume over:
387
395
  // that run records the outcome.
388
396
  store.withClient(function (client) {
389
- return finishResume(client, wait, runStatus(error, value), error ? store.errorMessage(error) : null, resumedAt);
397
+ return finishResume(client, wait, runStatus(error, value), runNote(error, value), resumedAt);
390
398
  }).catch(function (writeError) {
391
399
  console.error('[automations] could not record a late resume: ' + store.errorMessage(writeError));
392
400
  });
@@ -624,7 +632,7 @@ async function deliverEvent(client, row, context) {
624
632
  var error = ran.error ? store.errorMessage(ran.error) : null;
625
633
  if (error) console.error('[automations] ' + entry.name + ' failed for event ' + row.id + ': ' + error);
626
634
  // A run that stopped at a Wait is not finished: its delivery waits too.
627
- var finished = await client.query(FINISH_DELIVERY_SQL, [delivery.id, runStatus(ran.error, ran.value), error, delivery.attempts]);
635
+ var finished = await client.query(FINISH_DELIVERY_SQL, [delivery.id, runStatus(ran.error, ran.value), runNote(ran.error, ran.value), delivery.attempts]);
628
636
  if (resumePoint && !error && wrote(finished)) await client.query(SETTLE_FAILED_WAIT_SQL, [resumePoint.id]);
629
637
  if (error) outcome.failed += 1;
630
638
  else outcome.delivered += 1;
@@ -690,7 +698,7 @@ async function resumeWaits(client, context) {
690
698
  wait,
691
699
  eventRow,
692
700
  runStatus(ran.error, ran.value),
693
- ran.error ? store.errorMessage(ran.error) : null,
701
+ runNote(ran.error, ran.value),
694
702
  resumedAt
695
703
  );
696
704
  }
@@ -834,7 +842,7 @@ async function runTest(input, options) {
834
842
  }, timeoutMs, lateDeliveryWriter(delivery, true));
835
843
  if (ran.timedOut) return totals;
836
844
  var wouldWait = ran.value && ran.value.wouldWait ? ran.value.wouldWait : null;
837
- var note = ran.error ? store.errorMessage(ran.error) : wouldWait ? JSON.stringify({ wouldWait: wouldWait }) : null;
845
+ var note = wouldWait ? JSON.stringify({ wouldWait: wouldWait }) : runNote(ran.error, ran.value);
838
846
  await client.query(FINISH_DELIVERY_SQL, [delivery.id, 'test', note, delivery.attempts]);
839
847
  if (ran.error) totals.failed = 1;
840
848
  else totals.delivered = 1;
@@ -1008,6 +1008,9 @@ async function executeWorkflow(workflowConfig, triggerContext, nodeHandlers, opt
1008
1008
  if (triggerContext && triggerContext.__dynamicRouteParam) {
1009
1009
  context.__dynamicRouteParam = triggerContext.__dynamicRouteParam;
1010
1010
  }
1011
+ if (triggerContext && triggerContext.__tpa) {
1012
+ context.__tpa = triggerContext.__tpa;
1013
+ }
1011
1014
  if (triggerContext && triggerContext.triggerElement) {
1012
1015
  context.triggerElement = triggerContext.triggerElement;
1013
1016
  }
@@ -2151,6 +2154,7 @@ function buildContext(workflowConfig, triggerContext) {
2151
2154
  if (triggerContext.__stateValues) context.__stateValues = triggerContext.__stateValues;
2152
2155
  if (triggerContext.__routeParams) context.__routeParams = triggerContext.__routeParams;
2153
2156
  if (triggerContext.__dynamicRouteParam) context.__dynamicRouteParam = triggerContext.__dynamicRouteParam;
2157
+ if (triggerContext.__tpa) context.__tpa = triggerContext.__tpa;
2154
2158
  if (triggerContext.triggerElement) context.triggerElement = triggerContext.triggerElement;
2155
2159
  }
2156
2160
  // See executeWorkflow: the page's language travels with the run.
@@ -103,12 +103,18 @@ const adviceFor = (staffRoles: string[]): string =>
103
103
  const SHORT_ADVICE =
104
104
  'Let only an admin role start it (a role your admin-panel pages admit), or move it into a scheduled, signed webhook or automation workflow, then publish again.'
105
105
 
106
+ /** What to change on a site without accounts: no role can be required until it has them. */
107
+ const NO_ACCOUNTS_ADVICE =
108
+ 'Your site has no sign-in, so no role can be required yet: enable authentication and let only an admin role start it, or move it into a scheduled, signed webhook or automation workflow, then publish again.'
109
+
106
110
  /** One line of plain prose, for the person publishing. */
107
111
  export const describePublicStepRefusals = (
108
112
  refusals: PublicStepRefusal[],
109
- staffRoles: string[] = [TableAccess.ADMIN_ROLE]
113
+ staffRoles: string[] = [TableAccess.ADMIN_ROLE],
114
+ hasAccounts = true
110
115
  ): string => {
111
- const advice = adviceFor(staffRoles)
116
+ const advice = hasAccounts ? adviceFor(staffRoles) : NO_ACCOUNTS_ADVICE
117
+ const shortAdvice = hasAccounts ? SHORT_ADVICE : NO_ACCOUNTS_ADVICE
112
118
  const listed = refusals.slice(0, LISTED_STEPS).map(describe)
113
119
  const more = refusals.length - listed.length
114
120
  const subject =
@@ -121,10 +127,10 @@ export const describePublicStepRefusals = (
121
127
  )}${more > 0 ? `; and ${more} more` : ''}.`
122
128
  for (const candidate of [
123
129
  `${subject} ${advice}`,
124
- `${subject} ${SHORT_ADVICE}`,
130
+ `${subject} ${shortAdvice}`,
125
131
  `On your site the step ${describe(refusals[0])}${
126
132
  refusals.length > 1 ? `, and ${refusals.length - 1} more` : ''
127
- }. ${SHORT_ADVICE}`,
133
+ }. ${shortAdvice}`,
128
134
  ]) {
129
135
  if (candidate.length <= MAX_SENTENCE_LENGTH) {
130
136
  return candidate
@@ -132,7 +138,7 @@ export const describePublicStepRefusals = (
132
138
  }
133
139
  return `On your site ${
134
140
  refusals.length === 1 ? 'a step' : `${refusals.length} steps`
135
- } meant only for an admin or your own server (a push notification or a stored-file step) can be started by others. ${SHORT_ADVICE}`
141
+ } meant only for an admin or your own server (a push notification or a stored-file step) can be started by others. ${shortAdvice}`
136
142
  }
137
143
 
138
144
  /**
@@ -147,11 +153,11 @@ export class PublicStepRefusalError extends Error {
147
153
  readonly code = PUBLIC_STEP_REFUSAL_CODE
148
154
  readonly refusals: PublicStepRefusal[]
149
155
 
150
- constructor(refusals: PublicStepRefusal[], staffRoles?: string[]) {
156
+ constructor(refusals: PublicStepRefusal[], staffRoles?: string[], hasAccounts?: boolean) {
151
157
  super(
152
158
  `Publishing refused: ${JSON.stringify({
153
159
  code: PUBLIC_STEP_REFUSAL_CODE,
154
- message: describePublicStepRefusals(refusals, staffRoles),
160
+ message: describePublicStepRefusals(refusals, staffRoles, hasAccounts),
155
161
  steps: refusals.map((r) => ({ owner: r.ownerId, node: r.nodeId, type: r.nodeType })),
156
162
  })}`
157
163
  )
@@ -443,6 +449,6 @@ export const assertNoPublicRestrictedSteps = (
443
449
  const staffRoles = TableAccess.resolveStaffRoles(authentication)
444
450
  const refusals = findPublicRestrictedSteps(workflows, { staffRoles })
445
451
  if (refusals.length > 0) {
446
- throw new PublicStepRefusalError(refusals, staffRoles)
452
+ throw new PublicStepRefusalError(refusals, staffRoles, authentication?.enabled === true)
447
453
  }
448
454
  }
@@ -168,7 +168,7 @@ const publicStepRefusals = (
168
168
  ownerId: refusal.ownerId,
169
169
  ownerKind: refusal.ownerKind,
170
170
  nodeId: refusal.nodeId,
171
- message: describePublicStepRefusals([refusal], staffRoles),
171
+ message: describePublicStepRefusals([refusal], staffRoles, authentication?.enabled === true),
172
172
  }))
173
173
  }
174
174