@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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
'
|
|
269
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
await utils.
|
|
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.
|
|
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
|
};
|
|
@@ -527,9 +527,12 @@ function __isStreamingAI(node, config) {
|
|
|
527
527
|
return config && config.streaming && __streamingAITypes.indexOf(node.type) >= 0;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
// One node's result to the browser — never one kept on the server
|
|
531
|
-
//
|
|
530
|
+
// One node's result to the browser — never one kept on the server nor a
|
|
531
|
+
// credential (the closing reply carries that one sealed), and a data node's
|
|
532
|
+
// rows without the users table's credentials (see segmentReply).
|
|
533
|
+
const __credentialResultIds = utils.credentialResultNodeIds(SEGMENT_CONFIG);
|
|
532
534
|
function __streamNodeResult(res, nodeId, result) {
|
|
535
|
+
if (__credentialResultIds[nodeId]) return;
|
|
533
536
|
const kept = SEGMENT_CONFIG.nodes.find(function(nd) { return nd.id === nodeId; });
|
|
534
537
|
if (kept && kept.config && kept.config.keepResultOnServer === true && !utils.isBranchingNodeType(kept.type)) return;
|
|
535
538
|
res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: nodeId, result: utils.withoutCredentialColumns(kept, result) }) + '\\n\\n');
|
|
@@ -873,7 +876,7 @@ module.exports = async function handler(req, res) {
|
|
|
873
876
|
}
|
|
874
877
|
const result = await nodeHandler(resolved, context);
|
|
875
878
|
if (result && result.__earlyResponse) {
|
|
876
|
-
await utils.
|
|
879
|
+
await utils.handOffPendingNodePromises(context);
|
|
877
880
|
if (streamStarted) {
|
|
878
881
|
res.write('data: ' + JSON.stringify({ type: 'error', error: (result.__earlyResponse.body && result.__earlyResponse.body.message) || 'Request rejected' }) + '\\n\\n');
|
|
879
882
|
res.end();
|
|
@@ -898,10 +901,9 @@ module.exports = async function handler(req, res) {
|
|
|
898
901
|
}
|
|
899
902
|
}
|
|
900
903
|
|
|
901
|
-
// See the non-streaming segment route:
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
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);
|
|
905
907
|
delete context.__request;
|
|
906
908
|
delete context.__pendingNodePromises;
|
|
907
909
|
// MUST be stripped before replying: the results payload below IS this
|
|
@@ -919,7 +921,7 @@ module.exports = async function handler(req, res) {
|
|
|
919
921
|
}
|
|
920
922
|
} catch (error) {
|
|
921
923
|
console.error('Streaming workflow segment error:', error);
|
|
922
|
-
if (__wfContext) { await utils.
|
|
924
|
+
if (__wfContext) { await utils.handOffPendingNodePromises(__wfContext); }
|
|
923
925
|
if (streamStarted) {
|
|
924
926
|
try {
|
|
925
927
|
res.write('data: ' + JSON.stringify({ type: 'error', error: error.message || 'Internal server error' }) + '\\n\\n');
|
package/src/auth-generator.ts
CHANGED
|
@@ -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
|
-
|
|
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]]) {
|
package/src/await-result.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 || !
|
|
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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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' &&
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { nodeRegistry } from './nodes'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The node types whose result is a credential, read off the node registry
|
|
5
|
+
* (`resultIsCredential`): today the password-hashing node. Its result stays on
|
|
6
|
+
* the server. A server segment replies it to the browser only sealed —
|
|
7
|
+
* encrypted with a key derived from the app secret, signed like every other
|
|
8
|
+
* result — and a later server segment opens it again, so a workflow that
|
|
9
|
+
* hashes in one segment and writes in another still works (see the server
|
|
10
|
+
* runtime: `sealResult`, `openSealedResults`).
|
|
11
|
+
*/
|
|
12
|
+
export const CREDENTIAL_RESULT_NODE_TYPES: string[] = Object.keys(nodeRegistry).filter(
|
|
13
|
+
(nodeType) => nodeRegistry[nodeType].resultIsCredential === true
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `credentialResultNodeIds(segmentConfig)`: the nodes of a segment whose
|
|
18
|
+
* result is a credential — every node of a credential type, and every map
|
|
19
|
+
* loop whose results are such a node's results (the loop's last body step, as
|
|
20
|
+
* the route runs it; a loop of loops follows its inner loop).
|
|
21
|
+
*/
|
|
22
|
+
export const generateCredentialResultsCode = (): string => `
|
|
23
|
+
var __CREDENTIAL_RESULT_NODE_TYPES = ${JSON.stringify(CREDENTIAL_RESULT_NODE_TYPES)};
|
|
24
|
+
|
|
25
|
+
function lastLoopBodyNodeId(loopId, sortedNodes, edges) {
|
|
26
|
+
var start = edges.find(function(e) { return e.source === loopId && e.sourceHandle === 'loop'; });
|
|
27
|
+
if (!start) return null;
|
|
28
|
+
var body = {};
|
|
29
|
+
var queue = [start.target];
|
|
30
|
+
while (queue.length > 0) {
|
|
31
|
+
var current = queue.shift();
|
|
32
|
+
if (body[current] || current === loopId) continue;
|
|
33
|
+
body[current] = true;
|
|
34
|
+
edges.forEach(function(e) {
|
|
35
|
+
if (e.source === current && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle) && e.target !== loopId) {
|
|
36
|
+
queue.push(e.target);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
var last = null;
|
|
41
|
+
sortedNodes.forEach(function(n) { if (body[n.id]) last = n.id; });
|
|
42
|
+
return last;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function credentialResultNodeIds(segmentConfig) {
|
|
46
|
+
var nodes = (segmentConfig && segmentConfig.nodes) || [];
|
|
47
|
+
var edges = (segmentConfig && segmentConfig.edges) || [];
|
|
48
|
+
var ids = {};
|
|
49
|
+
nodes.forEach(function(n) {
|
|
50
|
+
if (__CREDENTIAL_RESULT_NODE_TYPES.indexOf(n.type) !== -1) ids[n.id] = true;
|
|
51
|
+
});
|
|
52
|
+
var sortedNodes = nodes.slice().sort(function(a, b) { return (a.stepNumber || 0) - (b.stepNumber || 0); });
|
|
53
|
+
var mapLoops = nodes.filter(function(n) {
|
|
54
|
+
var loopType = n.config && n.config.loopType;
|
|
55
|
+
return n.type === 'general-loop' && !!loopType && loopType !== 'forEach';
|
|
56
|
+
});
|
|
57
|
+
var grew = true;
|
|
58
|
+
while (grew) {
|
|
59
|
+
grew = false;
|
|
60
|
+
mapLoops.forEach(function(loop) {
|
|
61
|
+
if (ids[loop.id]) return;
|
|
62
|
+
var last = lastLoopBodyNodeId(loop.id, sortedNodes, edges);
|
|
63
|
+
if (last && ids[last]) {
|
|
64
|
+
ids[loop.id] = true;
|
|
65
|
+
grew = true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
`
|
|
@@ -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
|
|
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 {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './email-locale'
|
|
6
6
|
import { generateSignedInDestinationCode } from './auth-return/signed-in-destination-code'
|
|
7
7
|
import { generateCredentialColumnsCode } from './credential-columns-code'
|
|
8
|
+
import { generateCredentialResultsCode } from './credential-results'
|
|
8
9
|
|
|
9
10
|
export interface SharedRuntimeUtilsOptions {
|
|
10
11
|
/**
|
|
@@ -654,9 +655,15 @@ function normalizeComparisonOperator(op) {
|
|
|
654
655
|
.toLowerCase();
|
|
655
656
|
}
|
|
656
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
|
+
|
|
657
664
|
function evaluateSingleComparison(config, context) {
|
|
658
|
-
var rawLeft = resolveValue(config
|
|
659
|
-
var rawRight = resolveValue(config
|
|
665
|
+
var rawLeft = resolveValue(comparisonSide(config, 'leftValue', 'leftOperand'), context);
|
|
666
|
+
var rawRight = resolveValue(comparisonSide(config, 'rightValue', 'rightOperand'), context);
|
|
660
667
|
const op = normalizeComparisonOperator(config.operator || 'equals');
|
|
661
668
|
var isUnary = op === 'is-empty' || op === 'is-not-empty' || op === 'is-truthy' || op === 'is-falsy' ||
|
|
662
669
|
op === 'is-null' || op === 'is-not-null' || op === 'is-true' || op === 'is-false';
|
|
@@ -666,7 +673,7 @@ function evaluateSingleComparison(config, context) {
|
|
|
666
673
|
switch (op) {
|
|
667
674
|
case '===': return left === right;
|
|
668
675
|
case '!==': return left !== right;
|
|
669
|
-
case '==': case 'equals': return left == right;
|
|
676
|
+
case '=': case '==': case 'equals': return left == right;
|
|
670
677
|
case '!=': case 'not-equals': return left != right;
|
|
671
678
|
case '>': case 'greater-than': return left > right;
|
|
672
679
|
case '<': case 'less-than': return left < right;
|
|
@@ -808,10 +815,16 @@ var __DATA_NODE_TYPES = {
|
|
|
808
815
|
'data-delete-item': true
|
|
809
816
|
};
|
|
810
817
|
|
|
811
|
-
//
|
|
812
|
-
|
|
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.
|
|
813
826
|
function isFireAndForgetNode(node) {
|
|
814
|
-
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;
|
|
815
828
|
return !!node.config && node.config.awaitResult === false;
|
|
816
829
|
}
|
|
817
830
|
|
|
@@ -1418,7 +1431,12 @@ var SEGMENT_REPLY_PRIVATE_KEYS = {
|
|
|
1418
1431
|
//
|
|
1419
1432
|
// A data node's rows are replied without the auth users table's credentials
|
|
1420
1433
|
// (see credential-columns-code.ts).
|
|
1434
|
+
//
|
|
1435
|
+
// A node whose result is a credential (a password hash) is left out here too,
|
|
1436
|
+
// whatever its config says: the server runtime replies it sealed, readable by
|
|
1437
|
+
// a later server segment only (see credential-results.ts).
|
|
1421
1438
|
${generateCredentialColumnsCode()}
|
|
1439
|
+
${generateCredentialResultsCode()}
|
|
1422
1440
|
// The nodes whose result the client branches on: never kept on the server.
|
|
1423
1441
|
function isBranchingNodeType(type) {
|
|
1424
1442
|
return type === 'general-if-statement' || type === 'general-switch';
|
|
@@ -1427,7 +1445,7 @@ function isBranchingNodeType(type) {
|
|
|
1427
1445
|
function segmentReply(segmentConfig, context, snapshot) {
|
|
1428
1446
|
var reply = {};
|
|
1429
1447
|
var own = {};
|
|
1430
|
-
var kept =
|
|
1448
|
+
var kept = credentialResultNodeIds(segmentConfig);
|
|
1431
1449
|
var nodes = (segmentConfig && segmentConfig.nodes) || [];
|
|
1432
1450
|
for (var ni = 0; ni < nodes.length; ni++) {
|
|
1433
1451
|
own[nodes[ni].id] = nodes[ni];
|
|
@@ -1514,6 +1532,7 @@ module.exports = {
|
|
|
1514
1532
|
snapshotIncomingContext,
|
|
1515
1533
|
segmentReply,
|
|
1516
1534
|
withoutCredentialColumns,
|
|
1535
|
+
credentialResultNodeIds,
|
|
1517
1536
|
evaluateCondition,
|
|
1518
1537
|
evaluateSingleComparison,
|
|
1519
1538
|
normalizeComparisonOperator,
|