@teleporthq/teleport-plugin-next-workflows 0.44.7 → 0.44.9

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 (141) 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__/auth-password-change-ends-sessions.test.ts +178 -0
  7. package/__tests__/credential-result-reply.test.ts +509 -0
  8. package/__tests__/data-api-invalid-body.test.ts +110 -0
  9. package/__tests__/data-node-not-awaited.test.ts +7 -6
  10. package/__tests__/invoice-payment-line.test.ts +68 -0
  11. package/__tests__/invoice-tender.test.ts +116 -0
  12. package/__tests__/pending-work-after-reply.test.ts +294 -0
  13. package/__tests__/runtime-condition-operands.test.ts +86 -0
  14. package/__tests__/webhook-delivery-lease.test.ts +68 -6
  15. package/dist/cjs/account-delete-route-generator.d.ts +7 -2
  16. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  17. package/dist/cjs/account-delete-route-generator.js +90 -43
  18. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  19. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  20. package/dist/cjs/api-route-generator.js +17 -15
  21. package/dist/cjs/api-route-generator.js.map +1 -1
  22. package/dist/cjs/auth-generator.d.ts.map +1 -1
  23. package/dist/cjs/auth-generator.js +34 -1
  24. package/dist/cjs/auth-generator.js.map +1 -1
  25. package/dist/cjs/await-result.d.ts +13 -5
  26. package/dist/cjs/await-result.d.ts.map +1 -1
  27. package/dist/cjs/await-result.js +19 -8
  28. package/dist/cjs/await-result.js.map +1 -1
  29. package/dist/cjs/credential-results.d.ts +18 -0
  30. package/dist/cjs/credential-results.d.ts.map +1 -0
  31. package/dist/cjs/credential-results.js +72 -0
  32. package/dist/cjs/credential-results.js.map +1 -0
  33. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  34. package/dist/cjs/data-api-route-generator.js +9 -1
  35. package/dist/cjs/data-api-route-generator.js.map +1 -1
  36. package/dist/cjs/executor-generator.d.ts.map +1 -1
  37. package/dist/cjs/executor-generator.js +26 -7
  38. package/dist/cjs/executor-generator.js.map +1 -1
  39. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  40. package/dist/cjs/invoice/invoice-html-code.js +96 -5
  41. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  42. package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
  43. package/dist/cjs/nodes/account/account-delete-current.js +4 -1
  44. package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
  45. package/dist/cjs/nodes/account/account-hash-password.d.ts.map +1 -1
  46. package/dist/cjs/nodes/account/account-hash-password.js +1 -0
  47. package/dist/cjs/nodes/account/account-hash-password.js.map +1 -1
  48. package/dist/cjs/nodes/types.d.ts +6 -0
  49. package/dist/cjs/nodes/types.d.ts.map +1 -1
  50. package/dist/cjs/server-runtime-code.d.ts +8 -0
  51. package/dist/cjs/server-runtime-code.d.ts.map +1 -1
  52. package/dist/cjs/server-runtime-code.js +127 -0
  53. package/dist/cjs/server-runtime-code.js.map +1 -1
  54. package/dist/cjs/transactional-email-code.d.ts +7 -0
  55. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  56. package/dist/cjs/transactional-email-code.js +5 -5
  57. package/dist/cjs/transactional-email-code.js.map +1 -1
  58. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  59. package/dist/cjs/vercel-functions.d.ts +34 -0
  60. package/dist/cjs/vercel-functions.d.ts.map +1 -0
  61. package/dist/cjs/vercel-functions.js +62 -0
  62. package/dist/cjs/vercel-functions.js.map +1 -0
  63. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  64. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  65. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  66. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  67. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  68. package/dist/cjs/workflow-project-plugin.js +3 -0
  69. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  70. package/dist/esm/account-delete-route-generator.d.ts +7 -2
  71. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  72. package/dist/esm/account-delete-route-generator.js +90 -43
  73. package/dist/esm/account-delete-route-generator.js.map +1 -1
  74. package/dist/esm/api-route-generator.d.ts.map +1 -1
  75. package/dist/esm/api-route-generator.js +17 -15
  76. package/dist/esm/api-route-generator.js.map +1 -1
  77. package/dist/esm/auth-generator.d.ts.map +1 -1
  78. package/dist/esm/auth-generator.js +34 -1
  79. package/dist/esm/auth-generator.js.map +1 -1
  80. package/dist/esm/await-result.d.ts +13 -5
  81. package/dist/esm/await-result.d.ts.map +1 -1
  82. package/dist/esm/await-result.js +18 -7
  83. package/dist/esm/await-result.js.map +1 -1
  84. package/dist/esm/credential-results.d.ts +18 -0
  85. package/dist/esm/credential-results.d.ts.map +1 -0
  86. package/dist/esm/credential-results.js +68 -0
  87. package/dist/esm/credential-results.js.map +1 -0
  88. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  89. package/dist/esm/data-api-route-generator.js +9 -1
  90. package/dist/esm/data-api-route-generator.js.map +1 -1
  91. package/dist/esm/executor-generator.d.ts.map +1 -1
  92. package/dist/esm/executor-generator.js +26 -7
  93. package/dist/esm/executor-generator.js.map +1 -1
  94. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  95. package/dist/esm/invoice/invoice-html-code.js +96 -5
  96. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  97. package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
  98. package/dist/esm/nodes/account/account-delete-current.js +4 -1
  99. package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
  100. package/dist/esm/nodes/account/account-hash-password.d.ts.map +1 -1
  101. package/dist/esm/nodes/account/account-hash-password.js +1 -0
  102. package/dist/esm/nodes/account/account-hash-password.js.map +1 -1
  103. package/dist/esm/nodes/types.d.ts +6 -0
  104. package/dist/esm/nodes/types.d.ts.map +1 -1
  105. package/dist/esm/server-runtime-code.d.ts +8 -0
  106. package/dist/esm/server-runtime-code.d.ts.map +1 -1
  107. package/dist/esm/server-runtime-code.js +127 -0
  108. package/dist/esm/server-runtime-code.js.map +1 -1
  109. package/dist/esm/transactional-email-code.d.ts +7 -0
  110. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  111. package/dist/esm/transactional-email-code.js +5 -5
  112. package/dist/esm/transactional-email-code.js.map +1 -1
  113. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  114. package/dist/esm/vercel-functions.d.ts +34 -0
  115. package/dist/esm/vercel-functions.d.ts.map +1 -0
  116. package/dist/esm/vercel-functions.js +57 -0
  117. package/dist/esm/vercel-functions.js.map +1 -0
  118. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  119. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  120. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  121. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  122. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  123. package/dist/esm/workflow-project-plugin.js +3 -0
  124. package/dist/esm/workflow-project-plugin.js.map +1 -1
  125. package/package.json +2 -2
  126. package/src/account-delete-route-generator.ts +97 -45
  127. package/src/api-route-generator.ts +17 -15
  128. package/src/auth-generator.ts +34 -1
  129. package/src/await-result.ts +19 -7
  130. package/src/credential-results.ts +71 -0
  131. package/src/data-api-route-generator.ts +9 -1
  132. package/src/executor-generator.ts +26 -7
  133. package/src/invoice/invoice-html-code.ts +96 -5
  134. package/src/nodes/account/account-delete-current.ts +4 -1
  135. package/src/nodes/account/account-hash-password.ts +1 -0
  136. package/src/nodes/types.ts +6 -0
  137. package/src/server-runtime-code.ts +128 -0
  138. package/src/transactional-email-code.ts +12 -5
  139. package/src/vercel-functions.ts +67 -0
  140. package/src/webhook-delivery-lease/webhook-delivery-lease-code.ts +26 -11
  141. package/src/workflow-project-plugin.ts +3 -0
@@ -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 = {}
@@ -47,6 +47,7 @@ async function account_hash_password(config: any, context: Record<string, unknow
47
47
  export const accountHashPassword: NodeHandlerGenerator = {
48
48
  nodeType: 'account-hash-password',
49
49
  executionEnv: 'server',
50
+ resultIsCredential: true,
50
51
  dependencies: {
51
52
  bcryptjs: '^2.4.3',
52
53
  },
@@ -114,6 +114,12 @@ export interface NodeHandlerGenerator {
114
114
  nodeType: string
115
115
  executionEnv: 'client' | 'server' | 'universal'
116
116
  isTerminal?: boolean
117
+ /**
118
+ * The node's result is a credential (a password hash). A server segment
119
+ * never replies it as it is: the browser receives it sealed, and only the
120
+ * app's own later server segments can open it (see credential-results.ts).
121
+ */
122
+ resultIsCredential?: boolean
117
123
  dependencies?: Record<string, string>
118
124
  generateHandler(): string
119
125
  generateServerHandler?(): string
@@ -1,3 +1,5 @@
1
+ import { generateVercelFunctionsLoaderCode } from './vercel-functions'
2
+
1
3
  /**
2
4
  * `utils/workflows/server-runtime.js`: what every server route loads. It is the
3
5
  * shared runtime utilities plus the parts only a server may run — the signed
@@ -27,6 +29,14 @@
27
29
  * that needs a trustworthy figure (a price, an owner) reads it from the
28
30
  * database itself.
29
31
  *
32
+ * A result that is a credential (a password hash, see credential-results.ts)
33
+ * makes the same trip SEALED: `segmentReply` replies it encrypted with a key
34
+ * derived from NEXTAUTH_SECRET (AES-256-GCM) and signs the sealed form, and
35
+ * `claimSegmentContext` opens every sealed value a request hands it (a
36
+ * relayed result once its signature holds). The browser relays it without
37
+ * ever reading it, and a later segment's reply hands back the sealed form,
38
+ * never the opened one.
39
+ *
30
40
  * The facts about the graph each route needs are baked into its
31
41
  * SEGMENT_CONFIG at generation time (see segment-trust.ts).
32
42
  */
@@ -34,6 +44,8 @@ export const generateServerRuntimeCode = (): string => `'use strict';
34
44
  var crypto = require('crypto');
35
45
  var utils = require('./runtime-utils');
36
46
 
47
+ ${generateVercelFunctionsLoaderCode()}
48
+
37
49
  var __stepKeyCache = { secret: null, key: null };
38
50
 
39
51
  function stepKey() {
@@ -51,6 +63,59 @@ function mac(text) {
51
63
  return crypto.createHmac('sha256', stepKey()).update(text).digest('hex');
52
64
  }
53
65
 
66
+ var __sealKeyCache = { secret: null, key: null };
67
+
68
+ function sealKey() {
69
+ var secret = process.env.NEXTAUTH_SECRET || '';
70
+ if (__sealKeyCache.secret !== secret) {
71
+ __sealKeyCache = {
72
+ secret: secret,
73
+ key: crypto.createHmac('sha256', secret).update('teleport-workflow-seal:v1').digest()
74
+ };
75
+ }
76
+ return __sealKeyCache.key;
77
+ }
78
+
79
+ var SEAL_IV_BYTES = 12;
80
+ var SEAL_TAG_BYTES = 16;
81
+
82
+ var SEAL_AAD = Buffer.from('teleport-workflow-sealed-result:v1', 'utf8');
83
+
84
+ // The sealed form of a credential result; undefined (the result is then not
85
+ // replied at all) without an app secret, whose key anyone could derive.
86
+ function sealResult(value) {
87
+ if (!process.env.NEXTAUTH_SECRET) return undefined;
88
+ var serialized;
89
+ try { serialized = JSON.stringify(value); } catch (e) { return undefined; }
90
+ if (serialized === undefined) return undefined;
91
+ var iv = crypto.randomBytes(SEAL_IV_BYTES);
92
+ var cipher = crypto.createCipheriv('aes-256-gcm', sealKey(), iv);
93
+ cipher.setAAD(SEAL_AAD);
94
+ var data = Buffer.concat([cipher.update(serialized, 'utf8'), cipher.final()]);
95
+ return { __sealed: Buffer.concat([iv, cipher.getAuthTag(), data]).toString('base64') };
96
+ }
97
+
98
+ function isSealedResult(value) {
99
+ return !!value && typeof value === 'object' && !Array.isArray(value) &&
100
+ typeof value.__sealed === 'string' && Object.keys(value).length === 1;
101
+ }
102
+
103
+ // { value } when the sealed form opens under this app's key, else null.
104
+ function openSealedResult(sealed) {
105
+ if (!process.env.NEXTAUTH_SECRET) return null;
106
+ try {
107
+ var raw = Buffer.from(sealed.__sealed, 'base64');
108
+ if (raw.length <= SEAL_IV_BYTES + SEAL_TAG_BYTES) return null;
109
+ var decipher = crypto.createDecipheriv('aes-256-gcm', sealKey(), raw.subarray(0, SEAL_IV_BYTES));
110
+ decipher.setAAD(SEAL_AAD);
111
+ decipher.setAuthTag(raw.subarray(SEAL_IV_BYTES, SEAL_IV_BYTES + SEAL_TAG_BYTES));
112
+ var data = Buffer.concat([decipher.update(raw.subarray(SEAL_IV_BYTES + SEAL_TAG_BYTES)), decipher.final()]);
113
+ return { value: JSON.parse(data.toString('utf8')) };
114
+ } catch (e) {
115
+ return null;
116
+ }
117
+ }
118
+
54
119
  // Without an app secret every key here would derive from '' — a value anyone
55
120
  // can compute — so nothing verifies: relayed server results are dropped and no
56
121
  // request passes as an internal call. (The generator gives every app with
@@ -206,10 +271,48 @@ function resetPreviousResult(segmentConfig, context) {
206
271
  // merge will.
207
272
  var __incomingOrders = new WeakMap();
208
273
 
274
+ // The sealed results this segment opened, so its reply hands the caller back
275
+ // the sealed form it relayed (see resealRelayedResults).
276
+ var __openedResults = new WeakMap();
277
+
278
+ // Opens every sealed value the request hands this segment, so its nodes read
279
+ // the credential itself: a relayed server result (whose signature
280
+ // dropUnsignedResults checked first), or a value the browser holds from one
281
+ // (a custom node's output, the previous result). Only this app's server
282
+ // seals, so what opens is what it sealed.
283
+ function openSealedResults(context) {
284
+ var opened = [];
285
+ Object.keys(context).forEach(function(key) {
286
+ if (isControlKey(key) && key !== '__previousNodeResult') return;
287
+ if (!isSealedResult(context[key])) return;
288
+ var result = openSealedResult(context[key]);
289
+ if (!result) return;
290
+ opened.push({ sealed: context[key], value: result.value });
291
+ context[key] = result.value;
292
+ });
293
+ __openedResults.set(context, opened);
294
+ }
295
+
296
+ // Every reply entry that is an opened value (the value itself, or the previous
297
+ // result taken from it) goes back sealed.
298
+ function resealRelayedResults(context, reply) {
299
+ var opened = __openedResults.get(context) || [];
300
+ if (opened.length === 0) return;
301
+ Object.keys(reply).forEach(function(key) {
302
+ for (var i = 0; i < opened.length; i++) {
303
+ if (reply[key] === opened[i].value) {
304
+ reply[key] = opened[i].sealed;
305
+ return;
306
+ }
307
+ }
308
+ });
309
+ }
310
+
209
311
  function claimSegmentContext(segmentConfig, context) {
210
312
  var orderKeys = readSignedOrder(context);
211
313
  if (orderKeys) restoreOrder(context, orderKeys);
212
314
  dropUnsignedResults(segmentConfig, context, orderKeys);
315
+ openSealedResults(context);
213
316
  stripForeignControlKeys(segmentConfig, context);
214
317
  __incomingOrders.set(context, Object.keys(context).filter(function(k) { return !isControlKey(k); }));
215
318
  utils.claimSegmentContext(segmentConfig, context);
@@ -219,6 +322,13 @@ function claimSegmentContext(segmentConfig, context) {
219
322
 
220
323
  function segmentReply(segmentConfig, context, snapshot) {
221
324
  var reply = utils.segmentReply(segmentConfig, context, snapshot);
325
+ resealRelayedResults(context, reply);
326
+ var credentialIds = utils.credentialResultNodeIds(segmentConfig);
327
+ Object.keys(credentialIds).forEach(function(id) {
328
+ if (context[id] === undefined) return;
329
+ var sealed = sealResult(context[id]);
330
+ if (sealed) reply[id] = sealed;
331
+ });
222
332
  var sigs = {};
223
333
  var incoming = context.__sig && typeof context.__sig === 'object' && !Array.isArray(context.__sig) ? context.__sig : {};
224
334
  Object.keys(incoming).forEach(function(id) {
@@ -288,7 +398,25 @@ function trustedBaseUrl(req) {
288
398
  return origin;
289
399
  }
290
400
 
401
+ // The work a segment's not-awaited steps left running (an invoice being
402
+ // rendered and mailed) must still finish, but the visitor need not wait for
403
+ // it: on Vercel it is handed to waitUntil, which keeps the function running
404
+ // after the reply. Anywhere else the route awaits it before replying, since a
405
+ // function frozen after its reply would drop it.
406
+ function handOffPendingNodePromises(context) {
407
+ var queue = context && context.__pendingNodePromises;
408
+ if (!queue || queue.length === 0) return Promise.resolve();
409
+ // Drained through the queue itself: the route deletes it from the context
410
+ // before replying, and a parallel branch still running pushes onto it.
411
+ var drained = utils.settlePendingNodePromises({ __pendingNodePromises: queue });
412
+ var waitUntil = __vercelWaitUntil();
413
+ if (!waitUntil) return drained;
414
+ waitUntil(drained);
415
+ return Promise.resolve();
416
+ }
417
+
291
418
  module.exports = Object.assign({}, utils, {
419
+ handOffPendingNodePromises: handOffPendingNodePromises,
292
420
  claimSegmentContext: claimSegmentContext,
293
421
  segmentReply: segmentReply,
294
422
  internalRequestHeaders: internalRequestHeaders,
@@ -38,10 +38,17 @@ export interface TransactionalEmailLedger {
38
38
  source: string
39
39
  sourceRef: string
40
40
  relativePrefix: string
41
+ /**
42
+ * Records the send without the recipient's details: `msg.recordedTo` in
43
+ * place of the address, and neither the body nor the values it was filled
44
+ * with. For an email to someone whose details the route is removing (the
45
+ * farewell after an account deletion).
46
+ */
47
+ withoutRecipientDetails?: boolean
41
48
  }
42
49
 
43
- // Provider-specific `__sendProviderEmail({ from, fromName?, to, subject, html,
44
- // apiKey, tokenValues?, userId? })`, preceded by the `__emailSender` helper that
50
+ // Provider-specific `__sendProviderEmail({ from, fromName?, to, recordedTo?,
51
+ // subject, html, apiKey, tokenValues?, userId? })`, preceded by the `__emailSender` helper that
45
52
  // names the sender. No attachment (unlike the invoice sender). When no provider
46
53
  // is configured the function is a no-op so the route stays self-contained and
47
54
  // never throws. With `ledger`, the provider function is emitted as
@@ -77,12 +84,12 @@ async function __sendProviderEmail(msg) {
77
84
  __sentEmailLog.recordSentEmail({
78
85
  emailType: ${JSON.stringify(ledger.emailType)},
79
86
  audience: 'customer',
80
- to: msg.to,
87
+ to: ${ledger.withoutRecipientDetails ? 'msg.recordedTo' : 'msg.to'},
81
88
  from: sender.email,
82
89
  fromName: sender.name,
83
90
  subject: msg.subject,
84
- html: msg.html,
85
- payload: msg.tokenValues,
91
+ html: ${ledger.withoutRecipientDetails ? "''" : 'msg.html'},
92
+ payload: ${ledger.withoutRecipientDetails ? 'null' : 'msg.tokenValues'},
86
93
  provider: ${JSON.stringify(provider)},
87
94
  status: failure ? 'failed' : 'sent',
88
95
  error: failure ? (failure.message || String(failure)) : null,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `@vercel/functions` in a generated project: `waitUntil`, which keeps a
3
+ * Vercel function running after its reply, and `attachDatabasePool`, which
4
+ * closes a pool's idle connections before the function is suspended.
5
+ *
6
+ * The generated code loads it inside a try, so a project that does not have
7
+ * it installed (or a bundler that cannot find it) keeps working without
8
+ * either. A project whose emitted files require it declares it (see
9
+ * `declareVercelFunctionsWhereRequired`).
10
+ */
11
+ export const VERCEL_FUNCTIONS_PACKAGE = '@vercel/functions'
12
+
13
+ // The 2.x line still supports Node 18, which the Next project template allows;
14
+ // 3.x requires Node 20. attachDatabasePool is stable from 2.2.8.
15
+ export const VERCEL_FUNCTIONS_VERSION = '^2.2.13'
16
+
17
+ const VERCEL_FUNCTIONS_REQUIRE = `require('${VERCEL_FUNCTIONS_PACKAGE}')`
18
+
19
+ /**
20
+ * `__vercelFunctions()` (the module, or null) and `__vercelWaitUntil()` (the
21
+ * module's waitUntil while this code runs inside a Vercel request, else null).
22
+ *
23
+ * waitUntil does nothing outside a Vercel request (a local `next dev`, another
24
+ * host): it reads the request context Vercel installs under
25
+ * `Symbol.for('@vercel/request-context')` and silently drops the promise when
26
+ * there is none. The same check here lets a caller await the work instead.
27
+ */
28
+ export const generateVercelFunctionsLoaderCode = (): string => `var __vercelFunctionsModule;
29
+ function __vercelFunctions() {
30
+ if (__vercelFunctionsModule === undefined) {
31
+ try { __vercelFunctionsModule = ${VERCEL_FUNCTIONS_REQUIRE}; } catch (_e) { __vercelFunctionsModule = null; }
32
+ }
33
+ return __vercelFunctionsModule;
34
+ }
35
+
36
+ function __vercelWaitUntil() {
37
+ var store = globalThis[Symbol.for('@vercel/request-context')];
38
+ var requestContext = store && typeof store.get === 'function' ? store.get() : null;
39
+ if (!requestContext || typeof requestContext.waitUntil !== 'function') return null;
40
+ var vercel = __vercelFunctions();
41
+ return vercel && typeof vercel.waitUntil === 'function' ? vercel.waitUntil : null;
42
+ }`
43
+
44
+ interface GeneratedFileRecord {
45
+ files?: Array<{ content?: unknown }>
46
+ }
47
+
48
+ /**
49
+ * Declares `@vercel/functions` in the project's dependencies when any emitted
50
+ * file requires it, keeping a version the project already declares.
51
+ */
52
+ export const declareVercelFunctionsWhereRequired = (
53
+ files: Map<string, GeneratedFileRecord>,
54
+ dependencies: Record<string, string>
55
+ ): void => {
56
+ if (dependencies[VERCEL_FUNCTIONS_PACKAGE]) {
57
+ return
58
+ }
59
+ for (const record of files.values()) {
60
+ for (const file of record.files || []) {
61
+ if (typeof file.content === 'string' && file.content.includes(VERCEL_FUNCTIONS_REQUIRE)) {
62
+ dependencies[VERCEL_FUNCTIONS_PACKAGE] = VERCEL_FUNCTIONS_VERSION
63
+ return
64
+ }
65
+ }
66
+ }
67
+ }
@@ -22,6 +22,12 @@ import {
22
22
  * released before the route replies; one left by a run that died expires
23
23
  * after a lease longer than any route may run.
24
24
  *
25
+ * The table is used when it exists (the editor creates it with the payment
26
+ * tables) and created only when it does not: a site's database role may read
27
+ * and write its tables without being allowed to create one, and
28
+ * CREATE TABLE IF NOT EXISTS asks for that permission even when the table is
29
+ * there.
30
+ *
25
31
  * Contract: `claimWebhookDelivery` never throws. Without Postgres, a driver,
26
32
  * the table or the privilege to create it, it claims nothing and lets the
27
33
  * delivery run — as every delivery ran before the lease existed.
@@ -48,6 +54,7 @@ ${generateLazyPgClientCode()}
48
54
  var TABLE_NAME = '${WEBHOOK_DELIVERY_LEASE_TABLE}';
49
55
  var LEASE_SECONDS = ${WEBHOOK_DELIVERY_LEASE_SECONDS};
50
56
  var QUERY_TIMEOUT_MS = 5000;
57
+ var TABLE_EXISTS_SQL = 'SELECT to_regclass($1) IS NOT NULL AS present';
51
58
  var CREATE_SQL = 'CREATE TABLE IF NOT EXISTS "' + TABLE_NAME + '" (' +
52
59
  'delivery_key TEXT PRIMARY KEY, claim_token TEXT NOT NULL, ' +
53
60
  'claimed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), attempts INTEGER NOT NULL DEFAULT 1)';
@@ -80,20 +87,28 @@ function __withTimeout(promise, ms) {
80
87
  return Promise.race([promise, timeout]).then(function (v) { clearTimeout(timer); return v; }, function (err) { clearTimeout(timer); throw err; });
81
88
  }
82
89
 
83
- async function __run(statements) {
90
+ // Runs \`work(query)\` on one connection, closed afterwards.
91
+ async function __run(work) {
84
92
  var client = getClient();
85
93
  await __withTimeout(client.connect(), QUERY_TIMEOUT_MS);
86
94
  try {
87
- var result = null;
88
- for (var i = 0; i < statements.length; i++) {
89
- result = await __withTimeout(client.query(statements[i].sql, statements[i].values), QUERY_TIMEOUT_MS);
90
- }
91
- return result;
95
+ return await work(function (sql, values) {
96
+ return __withTimeout(client.query(sql, values || []), QUERY_TIMEOUT_MS);
97
+ });
92
98
  } finally {
93
99
  try { await client.end(); } catch (_e) {}
94
100
  }
95
101
  }
96
102
 
103
+ async function __ensureTable(query) {
104
+ if (__tableReady) return;
105
+ var found = await query(TABLE_EXISTS_SQL, [TABLE_NAME]);
106
+ if (!found || !Array.isArray(found.rows) || !found.rows[0] || !found.rows[0].present) {
107
+ await query(CREATE_SQL);
108
+ }
109
+ __tableReady = true;
110
+ }
111
+
97
112
  function __stableJson(value) {
98
113
  try { return JSON.stringify(value === undefined ? null : value) || ''; } catch (_e) { return ''; }
99
114
  }
@@ -120,12 +135,12 @@ async function claimWebhookDelivery(scope, rawBody, verifiedBody) {
120
135
  .update(__stableJson(verifiedBody))
121
136
  .digest('hex');
122
137
  var token = crypto.randomBytes(16).toString('hex');
123
- var statements = __tableReady ? [] : [{ sql: CREATE_SQL, values: [] }];
124
- statements.push({ sql: CLAIM_SQL, values: [key, token, LEASE_SECONDS] });
125
138
  var result;
126
139
  try {
127
- result = await __run(statements);
128
- __tableReady = true;
140
+ result = await __run(async function (query) {
141
+ await __ensureTable(query);
142
+ return query(CLAIM_SQL, [key, token, LEASE_SECONDS]);
143
+ });
129
144
  } catch (err) {
130
145
  var code = err && err.code;
131
146
  if (code === '42501') { __disable('no privilege to create or write table ' + TABLE_NAME); }
@@ -138,7 +153,7 @@ async function claimWebhookDelivery(scope, rawBody, verifiedBody) {
138
153
  return {
139
154
  claimed: true,
140
155
  release: function () {
141
- return __run([{ sql: RELEASE_SQL, values: [key, token] }]).then(
156
+ return __run(function (query) { return query(RELEASE_SQL, [key, token]); }).then(
142
157
  function () {},
143
158
  function (err) { console.error('[webhook-delivery-lease] release failed: ' + (err && err.message ? err.message : String(err))); }
144
159
  );
@@ -72,6 +72,7 @@ import {
72
72
  import { generateInvoiceFiles, resolveInvoiceDataSource } from './invoice'
73
73
  import { ensureSentEmailLogModule, hasEmailSendingNodeType } from './sent-email-log'
74
74
  import { ensureWebhookDeliveryLeaseModule } from './webhook-delivery-lease'
75
+ import { declareVercelFunctionsWhereRequired } from './vercel-functions'
75
76
  import { ensureEmailLocaleModule, resolveEmailLocaleConfig } from './email-locale'
76
77
  import { workflowUtilsImportLine, workflowUtilsRequireLine } from './workflow-utils-alias'
77
78
  import { generateWebhookFiles } from './webhook-generator'
@@ -385,6 +386,7 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
385
386
  if (hasAuthentication) {
386
387
  this.injectSessionProviderIntoApp(structure)
387
388
  }
389
+ declareVercelFunctionsWhereRequired(files, dependencies)
388
390
  return structure
389
391
  }
390
392
 
@@ -974,6 +976,7 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
974
976
  this.injectSessionProviderIntoApp(structure)
975
977
  }
976
978
 
979
+ declareVercelFunctionsWhereRequired(files, dependencies)
977
980
  return structure
978
981
  }
979
982