@teleporthq/teleport-plugin-next-workflows 0.44.8 → 0.44.10

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 (172) hide show
  1. package/__tests__/_helpers/load-server-runtime.ts +6 -2
  2. package/__tests__/_helpers/run-account-delete-route.ts +140 -0
  3. package/__tests__/account-delete-current-password.test.ts +73 -0
  4. package/__tests__/account-delete-password-check.test.ts +113 -0
  5. package/__tests__/account-delete-visitor-details.test.ts +206 -0
  6. package/__tests__/ai-error-contract.test.ts +10 -0
  7. package/__tests__/auth-password-change-ends-sessions.test.ts +178 -0
  8. package/__tests__/continue-on-error.test.ts +27 -0
  9. package/__tests__/data-api-invalid-body.test.ts +110 -0
  10. package/__tests__/data-node-not-awaited.test.ts +7 -6
  11. package/__tests__/email-node-recipients.test.ts +288 -0
  12. package/__tests__/invoice-payment-line.test.ts +68 -0
  13. package/__tests__/invoice-tender.test.ts +116 -0
  14. package/__tests__/pending-work-after-reply.test.ts +294 -0
  15. package/__tests__/runtime-condition-operands.test.ts +86 -0
  16. package/__tests__/sent-email-log-module.test.ts +54 -0
  17. package/__tests__/webhook-delivery-lease.test.ts +68 -6
  18. package/dist/cjs/account-delete-route-generator.d.ts +7 -2
  19. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  20. package/dist/cjs/account-delete-route-generator.js +90 -43
  21. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  22. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  23. package/dist/cjs/api-route-generator.js +12 -13
  24. package/dist/cjs/api-route-generator.js.map +1 -1
  25. package/dist/cjs/auth-generator.d.ts.map +1 -1
  26. package/dist/cjs/auth-generator.js +34 -1
  27. package/dist/cjs/auth-generator.js.map +1 -1
  28. package/dist/cjs/await-result.d.ts +13 -5
  29. package/dist/cjs/await-result.d.ts.map +1 -1
  30. package/dist/cjs/await-result.js +19 -8
  31. package/dist/cjs/await-result.js.map +1 -1
  32. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  33. package/dist/cjs/data-api-route-generator.js +9 -1
  34. package/dist/cjs/data-api-route-generator.js.map +1 -1
  35. package/dist/cjs/executor-generator.d.ts.map +1 -1
  36. package/dist/cjs/executor-generator.js +22 -7
  37. package/dist/cjs/executor-generator.js.map +1 -1
  38. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  39. package/dist/cjs/invoice/invoice-html-code.js +96 -5
  40. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  41. package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
  42. package/dist/cjs/nodes/account/account-delete-current.js +4 -1
  43. package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
  44. package/dist/cjs/nodes/email/email-mailersend.d.ts.map +1 -1
  45. package/dist/cjs/nodes/email/email-mailersend.js +55 -48
  46. package/dist/cjs/nodes/email/email-mailersend.js.map +1 -1
  47. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  48. package/dist/cjs/nodes/email/email-mailgun.js +44 -32
  49. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  50. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  51. package/dist/cjs/nodes/email/email-postmark.js +44 -31
  52. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  53. package/dist/cjs/nodes/email/email-recipients.d.ts +28 -0
  54. package/dist/cjs/nodes/email/email-recipients.d.ts.map +1 -0
  55. package/dist/cjs/nodes/email/email-recipients.js +74 -0
  56. package/dist/cjs/nodes/email/email-recipients.js.map +1 -0
  57. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  58. package/dist/cjs/nodes/email/email-resend.js +42 -28
  59. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  60. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  61. package/dist/cjs/nodes/email/email-sendgrid.js +56 -41
  62. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  63. package/dist/cjs/sent-email-log/sent-email-log-code.d.ts.map +1 -1
  64. package/dist/cjs/sent-email-log/sent-email-log-code.js +17 -1
  65. package/dist/cjs/sent-email-log/sent-email-log-code.js.map +1 -1
  66. package/dist/cjs/server-runtime-code.d.ts.map +1 -1
  67. package/dist/cjs/server-runtime-code.js +21 -0
  68. package/dist/cjs/server-runtime-code.js.map +1 -1
  69. package/dist/cjs/transactional-email-code.d.ts +7 -0
  70. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  71. package/dist/cjs/transactional-email-code.js +5 -5
  72. package/dist/cjs/transactional-email-code.js.map +1 -1
  73. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  74. package/dist/cjs/vercel-functions.d.ts +34 -0
  75. package/dist/cjs/vercel-functions.d.ts.map +1 -0
  76. package/dist/cjs/vercel-functions.js +62 -0
  77. package/dist/cjs/vercel-functions.js.map +1 -0
  78. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  79. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  80. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  81. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  82. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  83. package/dist/cjs/workflow-project-plugin.js +3 -0
  84. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  85. package/dist/esm/account-delete-route-generator.d.ts +7 -2
  86. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  87. package/dist/esm/account-delete-route-generator.js +90 -43
  88. package/dist/esm/account-delete-route-generator.js.map +1 -1
  89. package/dist/esm/api-route-generator.d.ts.map +1 -1
  90. package/dist/esm/api-route-generator.js +12 -13
  91. package/dist/esm/api-route-generator.js.map +1 -1
  92. package/dist/esm/auth-generator.d.ts.map +1 -1
  93. package/dist/esm/auth-generator.js +34 -1
  94. package/dist/esm/auth-generator.js.map +1 -1
  95. package/dist/esm/await-result.d.ts +13 -5
  96. package/dist/esm/await-result.d.ts.map +1 -1
  97. package/dist/esm/await-result.js +18 -7
  98. package/dist/esm/await-result.js.map +1 -1
  99. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  100. package/dist/esm/data-api-route-generator.js +9 -1
  101. package/dist/esm/data-api-route-generator.js.map +1 -1
  102. package/dist/esm/executor-generator.d.ts.map +1 -1
  103. package/dist/esm/executor-generator.js +22 -7
  104. package/dist/esm/executor-generator.js.map +1 -1
  105. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  106. package/dist/esm/invoice/invoice-html-code.js +96 -5
  107. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  108. package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
  109. package/dist/esm/nodes/account/account-delete-current.js +4 -1
  110. package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
  111. package/dist/esm/nodes/email/email-mailersend.d.ts.map +1 -1
  112. package/dist/esm/nodes/email/email-mailersend.js +55 -48
  113. package/dist/esm/nodes/email/email-mailersend.js.map +1 -1
  114. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  115. package/dist/esm/nodes/email/email-mailgun.js +44 -32
  116. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  117. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  118. package/dist/esm/nodes/email/email-postmark.js +44 -31
  119. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  120. package/dist/esm/nodes/email/email-recipients.d.ts +28 -0
  121. package/dist/esm/nodes/email/email-recipients.d.ts.map +1 -0
  122. package/dist/esm/nodes/email/email-recipients.js +71 -0
  123. package/dist/esm/nodes/email/email-recipients.js.map +1 -0
  124. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  125. package/dist/esm/nodes/email/email-resend.js +42 -28
  126. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  127. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  128. package/dist/esm/nodes/email/email-sendgrid.js +56 -41
  129. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  130. package/dist/esm/sent-email-log/sent-email-log-code.d.ts.map +1 -1
  131. package/dist/esm/sent-email-log/sent-email-log-code.js +17 -1
  132. package/dist/esm/sent-email-log/sent-email-log-code.js.map +1 -1
  133. package/dist/esm/server-runtime-code.d.ts.map +1 -1
  134. package/dist/esm/server-runtime-code.js +21 -0
  135. package/dist/esm/server-runtime-code.js.map +1 -1
  136. package/dist/esm/transactional-email-code.d.ts +7 -0
  137. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  138. package/dist/esm/transactional-email-code.js +5 -5
  139. package/dist/esm/transactional-email-code.js.map +1 -1
  140. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  141. package/dist/esm/vercel-functions.d.ts +34 -0
  142. package/dist/esm/vercel-functions.d.ts.map +1 -0
  143. package/dist/esm/vercel-functions.js +57 -0
  144. package/dist/esm/vercel-functions.js.map +1 -0
  145. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  146. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  147. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  148. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  149. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  150. package/dist/esm/workflow-project-plugin.js +3 -0
  151. package/dist/esm/workflow-project-plugin.js.map +1 -1
  152. package/package.json +2 -2
  153. package/src/account-delete-route-generator.ts +97 -45
  154. package/src/api-route-generator.ts +12 -13
  155. package/src/auth-generator.ts +34 -1
  156. package/src/await-result.ts +19 -7
  157. package/src/data-api-route-generator.ts +9 -1
  158. package/src/executor-generator.ts +22 -7
  159. package/src/invoice/invoice-html-code.ts +96 -5
  160. package/src/nodes/account/account-delete-current.ts +4 -1
  161. package/src/nodes/email/email-mailersend.ts +72 -55
  162. package/src/nodes/email/email-mailgun.ts +51 -36
  163. package/src/nodes/email/email-postmark.ts +51 -35
  164. package/src/nodes/email/email-recipients.ts +86 -0
  165. package/src/nodes/email/email-resend.ts +49 -31
  166. package/src/nodes/email/email-sendgrid.ts +63 -44
  167. package/src/sent-email-log/sent-email-log-code.ts +17 -1
  168. package/src/server-runtime-code.ts +22 -0
  169. package/src/transactional-email-code.ts +12 -5
  170. package/src/vercel-functions.ts +67 -0
  171. package/src/webhook-delivery-lease/webhook-delivery-lease-code.ts +26 -11
  172. package/src/workflow-project-plugin.ts +3 -0
@@ -4,9 +4,14 @@
4
4
  *
5
5
  * The client node calls this route, which:
6
6
  * 1. resolves the current user from the NextAuth session (never trusting a
7
- * client-supplied id),
7
+ * client-supplied id) and, when the request carries a password, goes on
8
+ * only if it is the account's own,
8
9
  * 2. runs a single DB transaction that detaches + anonymises business/legal
9
- * records (orders, withdrawal requests, bookings) so tracking survives,
10
+ * records (orders, withdrawal requests, bookings, downloads, gift cards
11
+ * the user bought or received) so tracking survives, takes the user's
12
+ * email out of the activity log and the sent-email ledger, leaves the
13
+ * invoices already issued as they are (an invoice must keep naming its
14
+ * buyer),
10
15
  * deletes the user's personal data (reviews, cart, favourites, chat
11
16
  * history), deletes the rows the user owns in the project's own tables —
12
17
  * the ones the AI made for the site, each through the one column the
@@ -100,10 +105,29 @@ const ANONYMIZE_TABLES = [
100
105
  'teleport_subscriptions',
101
106
  'teleport_abandoned_carts',
102
107
  'teleport_voucher_redemptions',
108
+ 'teleport_downloads',
103
109
  ];
104
110
  const EMAIL_COLUMNS = ['billing_email', 'shipping_email', 'customer_email', 'email'];
105
111
  // A booking also names the customer and carries their phone; both are cleared.
106
- const CLEARED_COLUMNS = { teleport_bookings: ['customer_name', 'customer_phone'] };
112
+ // A download keeps its count against the order's limit, without who fetched it
113
+ // from where.
114
+ const CLEARED_COLUMNS = {
115
+ teleport_bookings: ['customer_name', 'customer_phone'],
116
+ teleport_downloads: ['ip_address'],
117
+ };
118
+ // Tables that name the user only by their email: each column holding it takes
119
+ // the anonymised address (a gift card stays redeemable by its code) or a blank
120
+ // (an activity-log line keeps what happened, not who did it).
121
+ const EMAIL_REFERENCES = [
122
+ { table: 'teleport_gift_cards', column: 'purchaser_email', replacement: 'anonymised' },
123
+ { table: 'teleport_gift_cards', column: 'recipient_email', replacement: 'anonymised' },
124
+ { table: 'teleport_events', column: 'actor', replacement: 'blank' },
125
+ ];
126
+ const SENT_EMAILS_TABLE = 'teleport_sent_emails';
127
+
128
+ function anonymisedEmailOf(userId) {
129
+ return String(DELETED_EMAIL_PATTERN).split('{{userId}}').join(String(userId));
130
+ }
107
131
 
108
132
  async function tableColumns(client, table) {
109
133
  var r = await client.query(
@@ -146,6 +170,36 @@ async function deleteRowsByEmail(client, table, column, email) {
146
170
  await client.query('DELETE FROM ' + table + ' WHERE ' + column + ' = $1', [email]);
147
171
  }
148
172
 
173
+ async function replaceEmailReferences(client, email, anonymisedEmail) {
174
+ if (!email) { return; }
175
+ for (var i = 0; i < EMAIL_REFERENCES.length; i++) {
176
+ var ref = EMAIL_REFERENCES[i];
177
+ var cols = await tableColumns(client, ref.table);
178
+ if (cols.indexOf(ref.column) === -1) { continue; }
179
+ await client.query(
180
+ 'UPDATE ' + ref.table + ' SET ' + ref.column + ' = $1 WHERE lower(' + ref.column + ') = lower($2)',
181
+ [ref.replacement === 'anonymised' ? anonymisedEmail : '', email]
182
+ );
183
+ }
184
+ }
185
+
186
+ // The emails the site sent the user stay on record (which email, when, through
187
+ // which provider), without the address, the body or the values it was filled
188
+ // with.
189
+ async function blankSentEmailsTo(client, email) {
190
+ if (!email) { return; }
191
+ var cols = await tableColumns(client, SENT_EMAILS_TABLE);
192
+ if (cols.indexOf('recipient_email') === -1) { return; }
193
+ var sets = ["recipient_email = ''"];
194
+ if (cols.indexOf('recipients') !== -1) { sets.push('recipients = NULL'); }
195
+ if (cols.indexOf('body_html') !== -1) { sets.push("body_html = ''"); }
196
+ if (cols.indexOf('payload') !== -1) { sets.push('payload = NULL'); }
197
+ await client.query(
198
+ 'UPDATE ' + SENT_EMAILS_TABLE + ' SET ' + sets.join(', ') + ' WHERE lower(recipient_email) = lower($1)',
199
+ [email]
200
+ );
201
+ }
202
+
149
203
  // Rows the user owns in the project's OWN tables — the ones the AI made for
150
204
  // this site (saved items, requests, reviews …). Their link to the user is
151
205
  // ON DELETE SET NULL, so they outlived the account with a blank owner, and the
@@ -259,44 +313,34 @@ async function deleteOwnedRows(client, userId) {
259
313
  }
260
314
  }
261
315
 
262
- // Invoices are a financial/tax record that must survive, but they store the
263
- // customer's PII denormalised and link to the user only through order_id (no
264
- // user_id). Scrub the PII in place, matching invoices whose order belongs to the
265
- // user. MUST run BEFORE the orders row is anonymised (its user_id is still set).
266
- var INVOICE_PII_NULL_COLUMNS = [
267
- 'customer_name',
268
- 'customer_address',
269
- 'customer_city',
270
- 'customer_state',
271
- 'customer_zip',
272
- 'customer_country',
273
- 'customer_vat',
274
- 'customer_phone',
275
- ];
276
-
277
- async function anonymizeInvoices(client, ordersTable, invoicesTable, userId, syntheticEmail) {
278
- var invCols = await tableColumns(client, invoicesTable);
279
- if (invCols.indexOf('order_id') === -1) { return; }
280
- var orderCols = await tableColumns(client, ordersTable);
281
- if (orderCols.indexOf('user_id') === -1) { return; }
282
- var sets = [];
283
- var params = [];
284
- if (invCols.indexOf('customer_email') !== -1) {
285
- params.push(syntheticEmail);
286
- sets.push('customer_email = $' + params.length);
316
+ // The password a request confirms the deletion with, or null when it names
317
+ // none. A request without one deletes as before; one with a password deletes
318
+ // only when it is the account's own (the editor's delete-account form asks for
319
+ // it).
320
+ function readRequestPassword(req) {
321
+ var body = req.body;
322
+ if (typeof body === 'string') {
323
+ try { body = JSON.parse(body); } catch (e) { body = null; }
287
324
  }
288
- for (var i = 0; i < INVOICE_PII_NULL_COLUMNS.length; i++) {
289
- if (invCols.indexOf(INVOICE_PII_NULL_COLUMNS[i]) !== -1) {
290
- sets.push(INVOICE_PII_NULL_COLUMNS[i] + ' = NULL');
291
- }
325
+ if (!body || typeof body !== 'object' || body.password === undefined || body.password === null) {
326
+ return null;
327
+ }
328
+ return String(body.password);
329
+ }
330
+
331
+ // The sign-in check (utils/auth/hash-password.js verifyPassword): bcrypt over
332
+ // the SHA-256 of the password and AUTH_PEPPER. An account without a stored
333
+ // password (a social sign-in) matches no password.
334
+ function passwordMatches(password, storedHash) {
335
+ if (!storedHash || password.length > 1000) { return false; }
336
+ var bcrypt = require('bcryptjs');
337
+ var crypto = require('crypto');
338
+ var prepared = crypto.createHash('sha256').update(password + (process.env.AUTH_PEPPER || '')).digest('base64');
339
+ try {
340
+ return bcrypt.compareSync(prepared, String(storedHash));
341
+ } catch (e) {
342
+ return false;
292
343
  }
293
- if (sets.length === 0) { return; }
294
- params.push(userId);
295
- await client.query(
296
- 'UPDATE ' + invoicesTable + ' SET ' + sets.join(', ') +
297
- ' WHERE order_id IN (SELECT id FROM ' + ordersTable + ' WHERE user_id = $' + params.length + ')',
298
- params
299
- );
300
344
  }
301
345
 
302
346
  ${generateFillTemplateFn()}
@@ -313,6 +357,7 @@ ${generateProviderSendFunction(provider, {
313
357
  source: 'account-delete',
314
358
  sourceRef: 'api/account/delete-current',
315
359
  relativePrefix: '../../..',
360
+ withoutRecipientDetails: true,
316
361
  })}
317
362
 
318
363
  // \`locale\` is the language of the page the deletion was requested from
@@ -328,7 +373,7 @@ async function sendFarewellEmail(toEmail, tokenValues, userId, locale) {
328
373
  var copy = resolveFarewellEmailCopy(locale);
329
374
  var subject = fillTemplate(copy.subject, tokenValues);
330
375
  var html = fillTemplate(copy.body, tokenValues);
331
- await __sendProviderEmail({ from: from, fromName: EMAIL_FROM_NAME, to: toEmail, subject: subject, html: html, apiKey: apiKey, tokenValues: tokenValues, userId: userId });
376
+ await __sendProviderEmail({ from: from, fromName: EMAIL_FROM_NAME, to: toEmail, recordedTo: anonymisedEmailOf(userId), subject: subject, html: html, apiKey: apiKey, tokenValues: tokenValues, userId: userId });
332
377
  }
333
378
 
334
379
  module.exports = async function handler(req, res) {
@@ -348,10 +393,12 @@ module.exports = async function handler(req, res) {
348
393
  return res.status(401).json({ error: 'Unauthenticated' });
349
394
  }
350
395
 
396
+ var requestPassword = readRequestPassword(req);
351
397
  var client = getClient();
352
398
  var userEmail = '';
353
399
  var userName = '';
354
400
  var userFound = false;
401
+ var passwordRefused = false;
355
402
  var failed = false;
356
403
  // try/finally so the pg connection is ALWAYS closed — every early exit
357
404
  // (not-found, error) leaves via the finally, never leaking a connection.
@@ -364,14 +411,14 @@ module.exports = async function handler(req, res) {
364
411
  userFound = true;
365
412
  userEmail = userRes.rows[0].email || '';
366
413
  userName = userRes.rows[0].name || '';
367
-
368
- var syntheticEmail = String(DELETED_EMAIL_PATTERN).split('{{userId}}').join(String(userId));
414
+ passwordRefused =
415
+ requestPassword !== null && !passwordMatches(requestPassword, userRes.rows[0].password);
416
+ }
417
+ if (userFound && !passwordRefused) {
418
+ var syntheticEmail = anonymisedEmailOf(userId);
369
419
 
370
420
  await client.query('BEGIN');
371
421
  try {
372
- // Scrub invoice PII FIRST — invoices link to the user only through
373
- // order_id, so this must run while the orders row still carries user_id.
374
- await anonymizeInvoices(client, 'teleport_orders', 'teleport_invoices', userId, syntheticEmail);
375
422
  for (var a = 0; a < ANONYMIZE_TABLES.length; a++) {
376
423
  await anonymizeUserRows(client, ANONYMIZE_TABLES[a], userId, syntheticEmail);
377
424
  }
@@ -379,6 +426,8 @@ module.exports = async function handler(req, res) {
379
426
  await deleteUserRows(client, DELETE_TABLES[d], userId);
380
427
  }
381
428
  await deleteOwnedRows(client, userId);
429
+ await replaceEmailReferences(client, userEmail, syntheticEmail);
430
+ await blankSentEmailsTo(client, userEmail);
382
431
  // password_reset_tokens has no user_id — it is keyed by the email string.
383
432
  await deleteRowsByEmail(client, 'password_reset_tokens', 'email', userEmail);
384
433
  await client.query('DELETE FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
@@ -401,6 +450,9 @@ module.exports = async function handler(req, res) {
401
450
  if (!userFound) {
402
451
  return res.status(404).json({ error: 'Account not found' });
403
452
  }
453
+ if (passwordRefused) {
454
+ return res.status(403).json({ error: 'Incorrect password' });
455
+ }
404
456
 
405
457
  // Best-effort farewell email AFTER the successful commit (connection closed).
406
458
  try {
@@ -442,7 +442,7 @@ module.exports = async function handler(req, res) {
442
442
  for (var h = 0; h < hKeys.length; h++) {
443
443
  res.setHeader(hKeys[h], earlyRes.headers[hKeys[h]]);
444
444
  }
445
- await utils.settlePendingNodePromises(context);
445
+ await utils.handOffPendingNodePromises(context);
446
446
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
447
447
  return;
448
448
  }
@@ -453,11 +453,11 @@ module.exports = async function handler(req, res) {
453
453
  if (result && result.__terminal) break;
454
454
  }
455
455
 
456
- // Land every fire-and-forget query BEFORE replying: the platform may freeze
457
- // this function the moment the response is sent, which would drop the write.
458
- // The visitor does not pay for this — the client dispatched this segment
459
- // without awaiting it (see seg.fireAndForget in the client runtime).
460
- await utils.settlePendingNodePromises(context);
456
+ // Every not-awaited step must still land: the platform may freeze this
457
+ // function the moment the response is sent, which would drop the work. On
458
+ // Vercel it finishes after the reply (waitUntil); elsewhere the reply waits
459
+ // for it (see handOffPendingNodePromises in the server runtime).
460
+ await utils.handOffPendingNodePromises(context);
461
461
  delete context.__request;
462
462
  delete context.__pendingNodePromises;
463
463
  // MUST be stripped before replying: the results payload below IS this
@@ -469,7 +469,7 @@ module.exports = async function handler(req, res) {
469
469
  res.status(200).json({ success: true, results: utils.segmentReply(SEGMENT_CONFIG, context, __incomingSnapshot) });
470
470
  } catch (error) {
471
471
  console.error('Workflow segment error:', error);
472
- if (__wfContext) { await utils.settlePendingNodePromises(__wfContext); }
472
+ if (__wfContext) { await utils.handOffPendingNodePromises(__wfContext); }
473
473
  res.status(500).json({ success: false, error: error.message || 'Internal server error' });
474
474
  }
475
475
  };
@@ -876,7 +876,7 @@ module.exports = async function handler(req, res) {
876
876
  }
877
877
  const result = await nodeHandler(resolved, context);
878
878
  if (result && result.__earlyResponse) {
879
- await utils.settlePendingNodePromises(context);
879
+ await utils.handOffPendingNodePromises(context);
880
880
  if (streamStarted) {
881
881
  res.write('data: ' + JSON.stringify({ type: 'error', error: (result.__earlyResponse.body && result.__earlyResponse.body.message) || 'Request rejected' }) + '\\n\\n');
882
882
  res.end();
@@ -901,10 +901,9 @@ module.exports = async function handler(req, res) {
901
901
  }
902
902
  }
903
903
 
904
- // See the non-streaming segment route: land every fire-and-forget query
905
- // before the response completes, or the platform may freeze this function
906
- // with the write still in flight.
907
- await utils.settlePendingNodePromises(context);
904
+ // See the non-streaming segment route: every not-awaited step must still
905
+ // land, after the reply on Vercel, before it anywhere else.
906
+ await utils.handOffPendingNodePromises(context);
908
907
  delete context.__request;
909
908
  delete context.__pendingNodePromises;
910
909
  // MUST be stripped before replying: the results payload below IS this
@@ -922,7 +921,7 @@ module.exports = async function handler(req, res) {
922
921
  }
923
922
  } catch (error) {
924
923
  console.error('Streaming workflow segment error:', error);
925
- if (__wfContext) { await utils.settlePendingNodePromises(__wfContext); }
924
+ if (__wfContext) { await utils.handOffPendingNodePromises(__wfContext); }
926
925
  if (streamStarted) {
927
926
  try {
928
927
  res.write('data: ' + JSON.stringify({ type: 'error', error: error.message || 'Internal server error' }) + '\\n\\n');
@@ -673,7 +673,9 @@ const generateProvidersSetup = (auth: UIDLAuthentication): string => {
673
673
  if (!verifyPassword(String(credentials.password), user.password)) {
674
674
  throw new Error('InvalidCredentials');
675
675
  }
676
- return sanitizeUser(user);
676
+ const signedIn = sanitizeUser(user);
677
+ signedIn[TOKEN_PASSWORD_FINGERPRINT] = passwordFingerprint(user.password);
678
+ return signedIn;
677
679
  } catch (err) {
678
680
  if (err && err.message === 'InvalidCredentials') throw err;
679
681
  // A hashing/serialisation fault is an infrastructure problem, not a
@@ -758,6 +760,22 @@ const USER_REFRESH_INTERVAL_MS = ${USER_REFRESH_INTERVAL_MS};
758
760
  // internal timestamp on every binding that enumerates the user object.
759
761
  const TOKEN_REFRESH_STAMP = '__userRefreshedAt';
760
762
 
763
+ // A short fingerprint of the account's stored password hash, carried in the
764
+ // token from sign-in (or from the first refresh of a token that has none). A
765
+ // password change, a reset included, changes it, and the refresh then ends the
766
+ // session: every session opened before the change is signed out at its next
767
+ // refresh. Keyed with NEXTAUTH_SECRET; never the hash itself, and like the
768
+ // stamp above never copied onto \`session.user\`.
769
+ const TOKEN_PASSWORD_FINGERPRINT = '__passwordFingerprint';
770
+
771
+ function passwordFingerprint(storedHash) {
772
+ return require('crypto')
773
+ .createHmac('sha256', String(process.env.NEXTAUTH_SECRET || ''))
774
+ .update('teleport-password-fingerprint:v1|' + (storedHash ? String(storedHash) : ''))
775
+ .digest('base64')
776
+ .slice(0, 16);
777
+ }
778
+
761
779
  const authOptions = {
762
780
  providers: providers,
763
781
  pages: {
@@ -803,6 +821,7 @@ const authOptions = {
803
821
  if (params.trigger !== 'update' && isFresh) {
804
822
  return token;
805
823
  }
824
+ let passwordChanged = false;
806
825
  try {
807
826
  if (token && token.email && typeof findUserByEmail === 'function') {
808
827
  const fresh = await findUserByEmail(String(token.email));
@@ -813,10 +832,23 @@ const authOptions = {
813
832
  token[fk[i]] = safe[fk[i]];
814
833
  }
815
834
  }
835
+ if (fresh) {
836
+ const currentFingerprint = passwordFingerprint(fresh.password);
837
+ if (typeof token[TOKEN_PASSWORD_FINGERPRINT] !== 'string') {
838
+ token[TOKEN_PASSWORD_FINGERPRINT] = currentFingerprint;
839
+ } else if (token[TOKEN_PASSWORD_FINGERPRINT] !== currentFingerprint) {
840
+ passwordChanged = true;
841
+ }
842
+ }
816
843
  }
817
844
  } catch (e) {
818
845
  // Keep the existing token on any DB hiccup — never sign the user out.
819
846
  }
847
+ if (passwordChanged) {
848
+ // NextAuth answers a jwt callback that throws by clearing the session
849
+ // cookie and returning no session: the visitor is signed out.
850
+ throw new Error('The account password changed since this session signed in');
851
+ }
820
852
  // Stamped even when the read threw, so an unreachable database costs one
821
853
  // attempt per interval instead of one per request.
822
854
  if (token) {
@@ -830,6 +862,7 @@ const authOptions = {
830
862
  if (token && session.user) {
831
863
  const skip = { iat: 1, exp: 1, jti: 1, sub: 1 };
832
864
  skip[TOKEN_REFRESH_STAMP] = 1;
865
+ skip[TOKEN_PASSWORD_FINGERPRINT] = 1;
833
866
  const keys = Object.keys(token);
834
867
  for (let i = 0; i < keys.length; i++) {
835
868
  if (!skip[keys[i]]) {
@@ -15,6 +15,15 @@ export const DATA_NODE_TYPES: ReadonlyArray<string> = [
15
15
  'data-delete-item',
16
16
  ]
17
17
 
18
+ /**
19
+ * The other steps the author may choose not to wait for: server work whose
20
+ * result nothing downstream needs (a cash order's invoice, rendered, stored
21
+ * and mailed while the buyer is already on the confirmation page). The
22
+ * editor offers the choice on the same steps (`supportsAwaitResultOption` in
23
+ * teleport-gui's `@teleport/workflow-schema`) — a PAIRED EDIT.
24
+ */
25
+ export const NOT_AWAITABLE_SERVER_NODE_TYPES: ReadonlyArray<string> = ['ecommerce-generate-invoice']
26
+
18
27
  /**
19
28
  * Config key carrying the author's await choice. Only `false` opts out —
20
29
  * `undefined` (never configured) and `true` both mean "await", so every
@@ -22,14 +31,17 @@ export const DATA_NODE_TYPES: ReadonlyArray<string> = [
22
31
  */
23
32
  export const AWAIT_RESULT_CONFIG_KEY = 'awaitResult'
24
33
 
25
- const DATA_NODE_TYPE_SET = new Set<string>(DATA_NODE_TYPES)
34
+ const AWAIT_OPTIONAL_NODE_TYPE_SET = new Set<string>([
35
+ ...DATA_NODE_TYPES,
36
+ ...NOT_AWAITABLE_SERVER_NODE_TYPES,
37
+ ])
26
38
 
27
39
  /**
28
- * True when this node runs fire-and-forget: the runtime starts the query,
40
+ * True when this node runs fire-and-forget: the runtime starts the step,
29
41
  * publishes `null` under the node's id and moves straight on to the next node.
30
42
  */
31
43
  export const isFireAndForgetNode = (node: UIDLWorkflowNode | undefined | null): boolean => {
32
- if (!node || !DATA_NODE_TYPE_SET.has(node.type)) {
44
+ if (!node || !AWAIT_OPTIONAL_NODE_TYPE_SET.has(node.type)) {
33
45
  return false
34
46
  }
35
47
  const config = node.config as Record<string, unknown> | undefined
@@ -41,10 +53,10 @@ export const isFireAndForgetNode = (node: UIDLWorkflowNode | undefined | null):
41
53
  * response — i.e. every node it holds is fire-and-forget, so nothing
42
54
  * downstream can read anything the segment produces.
43
55
  *
44
- * The API route itself still awaits each query before replying (a serverless
45
- * function may be frozen the moment it responds, which would drop an in-flight
46
- * write); what this flag removes is the CLIENT's wait on that round trip, which
47
- * is where the latency the visitor feels actually comes from.
56
+ * The API route itself still lands each step (a serverless function may be
57
+ * frozen the moment it responds, which would drop an in-flight write: on
58
+ * Vercel the step finishes after the reply, elsewhere the reply waits); what
59
+ * this flag removes is the CLIENT's wait on that round trip.
48
60
  */
49
61
  export const isFireAndForgetSegment = (segment: WorkflowSegment): boolean =>
50
62
  segment.env === 'server' &&
@@ -1219,7 +1219,15 @@ module.exports = async function handler(req, res) {
1219
1219
  }
1220
1220
 
1221
1221
  var operation = params[params.length - 1];
1222
- var body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
1222
+ var body;
1223
+ try {
1224
+ body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
1225
+ } catch (e) {
1226
+ body = null;
1227
+ }
1228
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
1229
+ return res.status(400).json({ error: 'Invalid request body. Expected a JSON object.' });
1230
+ }
1223
1231
  var client = getClient();
1224
1232
 
1225
1233
  try {
@@ -655,9 +655,15 @@ function normalizeComparisonOperator(op) {
655
655
  .toLowerCase();
656
656
  }
657
657
 
658
+ // The editor's templates write a comparison's sides as leftOperand /
659
+ // rightOperand; everything else writes leftValue / rightValue.
660
+ function comparisonSide(config, valueKey, operandKey) {
661
+ return config[valueKey] !== undefined ? config[valueKey] : config[operandKey];
662
+ }
663
+
658
664
  function evaluateSingleComparison(config, context) {
659
- var rawLeft = resolveValue(config.leftValue, context);
660
- var rawRight = resolveValue(config.rightValue, context);
665
+ var rawLeft = resolveValue(comparisonSide(config, 'leftValue', 'leftOperand'), context);
666
+ var rawRight = resolveValue(comparisonSide(config, 'rightValue', 'rightOperand'), context);
661
667
  const op = normalizeComparisonOperator(config.operator || 'equals');
662
668
  var isUnary = op === 'is-empty' || op === 'is-not-empty' || op === 'is-truthy' || op === 'is-falsy' ||
663
669
  op === 'is-null' || op === 'is-not-null' || op === 'is-true' || op === 'is-false';
@@ -667,7 +673,7 @@ function evaluateSingleComparison(config, context) {
667
673
  switch (op) {
668
674
  case '===': return left === right;
669
675
  case '!==': return left !== right;
670
- case '==': case 'equals': return left == right;
676
+ case '=': case '==': case 'equals': return left == right;
671
677
  case '!=': case 'not-equals': return left != right;
672
678
  case '>': case 'greater-than': return left > right;
673
679
  case '<': case 'less-than': return left < right;
@@ -809,10 +815,16 @@ var __DATA_NODE_TYPES = {
809
815
  'data-delete-item': true
810
816
  };
811
817
 
812
- // A data node the author opted out of awaiting. Only an explicit \`false\` opts
813
- // out, so a config written before the option existed keeps awaiting.
818
+ // Mirrors NOT_AWAITABLE_SERVER_NODE_TYPES in await-result.ts — PAIRED EDIT.
819
+ var __NOT_AWAITABLE_SERVER_NODE_TYPES = {
820
+ 'ecommerce-generate-invoice': true
821
+ };
822
+
823
+ // A data node (or a step listed above) the author opted out of awaiting. Only
824
+ // an explicit \`false\` opts out, so a config written before the option existed
825
+ // keeps awaiting.
814
826
  function isFireAndForgetNode(node) {
815
- if (!node || !__DATA_NODE_TYPES[node.type]) return false;
827
+ if (!node || !(__DATA_NODE_TYPES[node.type] || __NOT_AWAITABLE_SERVER_NODE_TYPES[node.type])) return false;
816
828
  return !!node.config && node.config.awaitResult === false;
817
829
  }
818
830
 
@@ -880,9 +892,12 @@ async function settlePendingNodePromises(context) {
880
892
  // ({ error: '...' } / { success: false }) or through the AI-node contract
881
893
  // ({ error: true, message, code } — provider/auth failures). Both must halt
882
894
  // the workflow; treating error:true as success used to let the pipeline limp
883
- // into downstream NOT NULL violations.
895
+ // into downstream NOT NULL violations. A result that says success: true is
896
+ // the node's own verdict: an email node that reached some of its recipients
897
+ // reports success with the refused addresses under error, and goes on.
884
898
  function isFatalNodeResult(result) {
885
899
  if (!result) return false;
900
+ if (result.success === true) return false;
886
901
  return result.success === false ||
887
902
  (typeof result.error === 'string' && result.error) ||
888
903
  result.error === true;
@@ -265,6 +265,54 @@ function formatDateValue(raw) {
265
265
  }
266
266
  }
267
267
 
268
+ // How the invoice says it was paid: the method in words, with the provider that
269
+ // took the money when a provider did ("Card (Stripe)", "Paid online" for a
270
+ // booking paid through the site, "Cash on delivery", "Gift card"), and the
271
+ // provider's payment id as "Ref. pi_…". The stored values are ids.
272
+ var PAYMENT_PROVIDER_LABELS = {
273
+ stripe: 'Stripe', paypal: 'PayPal', mollie: 'Mollie', razorpay: 'Razorpay',
274
+ square: 'Square', paddle: 'Paddle', coingate: 'CoinGate',
275
+ };
276
+ var PAYMENT_METHOD_LABELS = {
277
+ card: 'Card',
278
+ online: 'Paid online',
279
+ cash_on_delivery: 'Cash on delivery',
280
+ cod: 'Cash on delivery',
281
+ cash: 'Cash',
282
+ store_pickup: 'Paid at pickup',
283
+ pickup: 'Paid at pickup',
284
+ gift_card: 'Gift card',
285
+ bank_transfer: 'Bank transfer',
286
+ };
287
+ var PAYMENT_METHODS_NAMING_PROVIDER = { card: true, online: true };
288
+
289
+ function humanizePaymentId(value) {
290
+ var words = String(value).replace(/[_-]+/g, ' ').trim();
291
+ return words.charAt(0).toUpperCase() + words.slice(1);
292
+ }
293
+
294
+ function paymentProviderLabel(provider) {
295
+ var id = String(provider || '').trim().toLowerCase();
296
+ return PAYMENT_PROVIDER_LABELS[id] || (id ? humanizePaymentId(id) : '');
297
+ }
298
+
299
+ function paymentMethodLabel(method, provider) {
300
+ var id = String(method || '').trim().toLowerCase();
301
+ var providerLabel = paymentProviderLabel(provider);
302
+ if (PAYMENT_METHOD_LABELS[id]) {
303
+ return PAYMENT_METHOD_LABELS[id] +
304
+ (PAYMENT_METHODS_NAMING_PROVIDER[id] && providerLabel ? ' (' + providerLabel + ')' : '');
305
+ }
306
+ if (PAYMENT_PROVIDER_LABELS[id]) return 'Paid online (' + PAYMENT_PROVIDER_LABELS[id] + ')';
307
+ if (!id) return providerLabel ? 'Paid online (' + providerLabel + ')' : '';
308
+ return humanizePaymentId(id);
309
+ }
310
+
311
+ function paymentReference(paymentId) {
312
+ var ref = String(paymentId || '').trim();
313
+ return ref ? 'Ref. ' + ref : '';
314
+ }
315
+
268
316
  function buildInvoiceDataScope(invoiceData) {
269
317
  var contextCompany = extractStaticAttrsFromContext(TEMPLATE_COMPONENT_UIDL).Company || {};
270
318
  var sym = invoiceData.currencySymbol || '$';
@@ -311,10 +359,13 @@ function buildInvoiceDataScope(invoiceData) {
311
359
  var giftCardLast4 = String(invoiceData.giftCardLast4 || '').trim();
312
360
  var totalNumber = Number(invoiceData.total);
313
361
  if (!isFinite(totalNumber)) totalNumber = 0;
362
+ // What is left to pay after the gift card. The invoice records what the
363
+ // provider or the courier was to collect; once that is paid, nothing is left.
314
364
  var amountDueNumber = Number(invoiceData.amountDue);
315
365
  if (invoiceData.amountDue == null || !isFinite(amountDueNumber)) {
316
366
  amountDueNumber = Math.max(0, totalNumber - giftCardAmountNumber);
317
367
  }
368
+ if (isSettledInvoice(invoiceData.status)) amountDueNumber = 0;
318
369
 
319
370
  // A paid invoice asks for nothing: where the due date would print, it says
320
371
  // when it was paid. The order keeps no payment method finer than its
@@ -376,9 +427,9 @@ function buildInvoiceDataScope(invoiceData) {
376
427
  realCustomer.hasVat = realCustomer.vat ? 'true' : 'false';
377
428
 
378
429
  var realPayment = {
379
- method: invoiceData.paymentMethod || '',
380
- provider: invoiceData.paymentProvider || '',
381
- intentId: invoiceData.paymentIntentId || '',
430
+ method: paymentMethodLabel(invoiceData.paymentMethod, invoiceData.paymentProvider),
431
+ provider: paymentProviderLabel(invoiceData.paymentProvider),
432
+ intentId: paymentReference(invoiceData.paymentIntentId),
382
433
  orderId: invoiceData.orderId || '',
383
434
  };
384
435
 
@@ -746,6 +797,39 @@ function renderComponentReference(content, scope, depth) {
746
797
  return rendered;
747
798
  }
748
799
 
800
+ // A paid invoice asks for nothing, so, as in the email, its amount-due line is
801
+ // left out. The template shows that line whenever a gift card was used, which
802
+ // cannot tell whether the rest was paid since. The line is the innermost
803
+ // container holding the field (the editor lays each summary line out as a row
804
+ // of texts), and only when the field is all it holds: a container that also
805
+ // holds a block or another field is the merchant's own, and stays.
806
+ var AMOUNT_DUE_FIELD = /\\bInvoice\\??\\.amountDue\\b/;
807
+
808
+ function isPaidAmountDueLine(content, scope) {
809
+ var inv = scope.invoiceData && scope.invoiceData.Invoice;
810
+ if (!inv || !isSettledInvoice(inv.status)) return false;
811
+ var fields = { amountDue: 0, other: 0 };
812
+ return countLineFields(content.children, fields) && fields.amountDue > 0 && fields.other === 0;
813
+ }
814
+
815
+ // Counts the fields a line prints into \`fields\`; false when it holds anything
816
+ // but text, which makes it a block rather than a line.
817
+ function countLineFields(children, fields) {
818
+ var list = children || [];
819
+ for (var i = 0; i < list.length; i++) {
820
+ var child = list[i];
821
+ if (!child || child.type === 'static') continue;
822
+ if (child.type === 'expr') {
823
+ if (AMOUNT_DUE_FIELD.test(String(child.content))) fields.amountDue++;
824
+ else fields.other++;
825
+ continue;
826
+ }
827
+ var isText = child.type === 'element' && child.content && child.content.elementType === 'text';
828
+ if (!isText || !countLineFields(child.content.children, fields)) return false;
829
+ }
830
+ return true;
831
+ }
832
+
749
833
  function renderNode(node, scope, componentName, depth) {
750
834
  if (!node) return '';
751
835
  if (typeof depth !== 'number') depth = 0;
@@ -801,6 +885,10 @@ function renderNode(node, scope, componentName, depth) {
801
885
  return fragChildren.map(function (c) { return renderNode(c, scope, componentName, depth); }).join('');
802
886
  }
803
887
 
888
+ if (elementType === 'container' && isPaidAmountDueLine(content, scope)) {
889
+ return '';
890
+ }
891
+
804
892
  var tag = content.semanticType || elementTypeToTag(elementType);
805
893
  var baseClass = classNameForNode(componentName, content.name);
806
894
  var referencedClasses = collectReferencedStyleClasses(content.referencedStyles);
@@ -1034,11 +1122,14 @@ function buildFallbackInvoiceHtml(scope) {
1034
1122
  }
1035
1123
  totalsRows += '<div style="display:flex;justify-content:space-between;font-weight:bold;border-top:2px solid #333;padding-top:4px;"><span>Total</span><span>' + escapeHtml(inv.total) + '</span></div>';
1036
1124
  // A gift card settles part (or all) of the total, so what is left to pay is
1037
- // printed beneath it — 0.00 included, which is the buyer's proof of that.
1125
+ // printed beneath it — 0.00 included, which is the buyer's proof of that —
1126
+ // until the invoice is paid, whose paid date says nothing is left.
1038
1127
  if (inv.hasGiftCard === 'true') {
1039
1128
  var giftCardLabel = 'Paid by gift card' + (inv.giftCardLast4 ? ' \\u2022\\u2022\\u2022\\u2022' + inv.giftCardLast4 : '');
1040
1129
  totalsRows += '<div style="display:flex;justify-content:space-between;margin-top:4px;margin-bottom:4px;"><span>' + escapeHtml(giftCardLabel) + '</span><span>\\u2212' + escapeHtml(inv.giftCardAmount) + '</span></div>';
1041
- totalsRows += '<div style="display:flex;justify-content:space-between;font-weight:bold;"><span>Amount due</span><span>' + escapeHtml(inv.amountDue) + '</span></div>';
1130
+ if (!isSettledInvoice(inv.status)) {
1131
+ totalsRows += '<div style="display:flex;justify-content:space-between;font-weight:bold;"><span>Amount due</span><span>' + escapeHtml(inv.amountDue) + '</span></div>';
1132
+ }
1042
1133
  }
1043
1134
  parts.push('<div style="display:flex;justify-content:flex-end;margin-top:16px;"><div style="width:240px;">' +
1044
1135
  totalsRows +
@@ -18,10 +18,13 @@ async function account_delete_current(config: any, context: Record<string, unkno
18
18
  if (typeof runLocale === 'string' && runLocale) {
19
19
  deleteHeaders['x-teleport-locale'] = runLocale
20
20
  }
21
+ // A template that asks for the password passes what the visitor typed; the
22
+ // route checks it the way sign-in does and deletes nothing when it is wrong.
23
+ const password = config && typeof config.password === 'string' ? config.password : ''
21
24
  const response = await fetch(baseUrl + '/api/account/delete-current', {
22
25
  method: 'POST',
23
26
  headers: deleteHeaders,
24
- body: '{}',
27
+ body: JSON.stringify(password ? { password } : {}),
25
28
  })
26
29
 
27
30
  let data: any = {}