@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.
- package/__tests__/_helpers/load-server-runtime.ts +6 -2
- package/__tests__/_helpers/run-account-delete-route.ts +140 -0
- package/__tests__/account-delete-current-password.test.ts +73 -0
- package/__tests__/account-delete-password-check.test.ts +113 -0
- package/__tests__/account-delete-visitor-details.test.ts +206 -0
- package/__tests__/auth-password-change-ends-sessions.test.ts +178 -0
- package/__tests__/credential-result-reply.test.ts +509 -0
- package/__tests__/data-api-invalid-body.test.ts +110 -0
- package/__tests__/data-node-not-awaited.test.ts +7 -6
- package/__tests__/invoice-payment-line.test.ts +68 -0
- package/__tests__/invoice-tender.test.ts +116 -0
- package/__tests__/pending-work-after-reply.test.ts +294 -0
- package/__tests__/runtime-condition-operands.test.ts +86 -0
- package/__tests__/webhook-delivery-lease.test.ts +68 -6
- package/dist/cjs/account-delete-route-generator.d.ts +7 -2
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +90 -43
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +17 -15
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +34 -1
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +13 -5
- package/dist/cjs/await-result.d.ts.map +1 -1
- package/dist/cjs/await-result.js +19 -8
- package/dist/cjs/await-result.js.map +1 -1
- package/dist/cjs/credential-results.d.ts +18 -0
- package/dist/cjs/credential-results.d.ts.map +1 -0
- package/dist/cjs/credential-results.js +72 -0
- package/dist/cjs/credential-results.js.map +1 -0
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +9 -1
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +26 -7
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +96 -5
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.js +4 -1
- package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-hash-password.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-hash-password.js +1 -0
- package/dist/cjs/nodes/account/account-hash-password.js.map +1 -1
- package/dist/cjs/nodes/types.d.ts +6 -0
- package/dist/cjs/nodes/types.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts +8 -0
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +127 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts +7 -0
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +5 -5
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/vercel-functions.d.ts +34 -0
- package/dist/cjs/vercel-functions.d.ts.map +1 -0
- package/dist/cjs/vercel-functions.js +62 -0
- package/dist/cjs/vercel-functions.js.map +1 -0
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +3 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +7 -2
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +90 -43
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +17 -15
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +34 -1
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +13 -5
- package/dist/esm/await-result.d.ts.map +1 -1
- package/dist/esm/await-result.js +18 -7
- package/dist/esm/await-result.js.map +1 -1
- package/dist/esm/credential-results.d.ts +18 -0
- package/dist/esm/credential-results.d.ts.map +1 -0
- package/dist/esm/credential-results.js +68 -0
- package/dist/esm/credential-results.js.map +1 -0
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +9 -1
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +26 -7
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +96 -5
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.js +4 -1
- package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
- package/dist/esm/nodes/account/account-hash-password.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-hash-password.js +1 -0
- package/dist/esm/nodes/account/account-hash-password.js.map +1 -1
- package/dist/esm/nodes/types.d.ts +6 -0
- package/dist/esm/nodes/types.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.d.ts +8 -0
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +127 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts +7 -0
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +5 -5
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/vercel-functions.d.ts +34 -0
- package/dist/esm/vercel-functions.d.ts.map +1 -0
- package/dist/esm/vercel-functions.js +57 -0
- package/dist/esm/vercel-functions.js.map +1 -0
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +3 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/account-delete-route-generator.ts +97 -45
- package/src/api-route-generator.ts +17 -15
- package/src/auth-generator.ts +34 -1
- package/src/await-result.ts +19 -7
- package/src/credential-results.ts +71 -0
- package/src/data-api-route-generator.ts +9 -1
- package/src/executor-generator.ts +26 -7
- package/src/invoice/invoice-html-code.ts +96 -5
- package/src/nodes/account/account-delete-current.ts +4 -1
- package/src/nodes/account/account-hash-password.ts +1 -0
- package/src/nodes/types.ts +6 -0
- package/src/server-runtime-code.ts +128 -0
- package/src/transactional-email-code.ts +12 -5
- package/src/vercel-functions.ts +67 -0
- package/src/webhook-delivery-lease/webhook-delivery-lease-code.ts +26 -11
- 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
|
-
|
|
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
|
},
|
package/src/nodes/types.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
128
|
-
|
|
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(
|
|
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
|
|