@teleporthq/teleport-plugin-next-workflows 0.43.21 → 0.43.26
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__/custom-js-loop-scope.test.ts +61 -0
- package/__tests__/email-body-context-resolution.test.ts +123 -0
- package/__tests__/middleware-home-route-protection.test.ts +91 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +25 -3
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +100 -3
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.js +14 -0
- package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +25 -3
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +100 -3
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.js +14 -0
- package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/auth-generator.ts +25 -3
- package/src/executor-generator.ts +100 -3
- package/src/nodes/general/general-custom-js.ts +14 -0
package/src/auth-generator.ts
CHANGED
|
@@ -966,11 +966,28 @@ function hasSessionCookie(request) {
|
|
|
966
966
|
return false;
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
// Where to send an authenticated user who lacks the required role. Normally the
|
|
970
|
+
// home page ("you don't have access, here's the public site"). But when "/" is
|
|
971
|
+
// itself a protected page (e.g. an admin dashboard published at the root), a
|
|
972
|
+
// redirect to "/" re-enters this middleware and loops forever — so fall back to
|
|
973
|
+
// the sign-in page (with a callbackUrl) in that case.
|
|
974
|
+
function roleDeniedRedirect(request, pathname) {
|
|
975
|
+
if (pathname !== '/' && !protectedRoutes['/']) {
|
|
976
|
+
return NextResponse.redirect(new URL('/', request.url));
|
|
977
|
+
}
|
|
978
|
+
var deniedUrl = new URL('${signInRoute}', request.url);
|
|
979
|
+
deniedUrl.searchParams.set('callbackUrl', pathname);
|
|
980
|
+
return NextResponse.redirect(deniedUrl);
|
|
981
|
+
}
|
|
982
|
+
|
|
969
983
|
async function middleware(request) {
|
|
970
984
|
const pathname = request.nextUrl.pathname;
|
|
971
985
|
|
|
972
986
|
for (let i = 0; i < authRoutes.length; i++) {
|
|
973
|
-
|
|
987
|
+
// Segment-safe: an auth route like "/sign-in" must not bypass protection on
|
|
988
|
+
// a same-prefix page such as "/sign-in-offers". Mirrors the protectedRoutes
|
|
989
|
+
// matching below.
|
|
990
|
+
if (pathname === authRoutes[i] || pathname.startsWith(authRoutes[i] + '/')) {
|
|
974
991
|
return NextResponse.next();
|
|
975
992
|
}
|
|
976
993
|
}
|
|
@@ -1044,7 +1061,7 @@ async function middleware(request) {
|
|
|
1044
1061
|
}
|
|
1045
1062
|
var userRole = getUserRoleFromToken(sessionUser);
|
|
1046
1063
|
if (userRole == null || allowedRoles.indexOf(userRole) < 0) {
|
|
1047
|
-
return
|
|
1064
|
+
return roleDeniedRedirect(request, pathname);
|
|
1048
1065
|
}
|
|
1049
1066
|
}
|
|
1050
1067
|
|
|
@@ -1053,7 +1070,12 @@ async function middleware(request) {
|
|
|
1053
1070
|
|
|
1054
1071
|
export default middleware;
|
|
1055
1072
|
export const config = {
|
|
1056
|
-
|
|
1073
|
+
// The bare '/' entry is required so middleware also runs on the home page:
|
|
1074
|
+
// Next.js does NOT run the second (negative-lookahead) matcher for the root
|
|
1075
|
+
// path, which would leave a page published at "/" (e.g. a protected dashboard)
|
|
1076
|
+
// publicly reachable. '/' compiles to ^/$ and covers exactly the home route;
|
|
1077
|
+
// the second entry covers every deeper path.
|
|
1078
|
+
matcher: ['/', '/((?!api|_next/static|_next/image|favicon\\\\.ico).*)'],
|
|
1057
1079
|
};
|
|
1058
1080
|
`
|
|
1059
1081
|
}
|
|
@@ -125,6 +125,102 @@ function isSecretRef(value) {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Resolve the inline workflow-context placeholders the rich-text email-body
|
|
129
|
+
// editor embeds for every dynamic value:
|
|
130
|
+
// <span class="context-value-inline" data-ctx-node-id="<id>"
|
|
131
|
+
// data-ctx-path='["<id>","result"]'>Label</span>
|
|
132
|
+
// resolveConfig leaves plain strings untouched, so without this the literal
|
|
133
|
+
// placeholder label (e.g. "Reset URL") would be delivered in the email instead
|
|
134
|
+
// of the resolved value. Handles both the raw single-quoted attribute form and
|
|
135
|
+
// the Quill-serialized "-escaped form, plus the nested inner <span> Quill
|
|
136
|
+
// wraps the label in.
|
|
137
|
+
function resolveRichTextContext(html, context) {
|
|
138
|
+
if (typeof html !== 'string' || html.indexOf('context-value-inline') === -1) {
|
|
139
|
+
return html;
|
|
140
|
+
}
|
|
141
|
+
var out = '';
|
|
142
|
+
var i = 0;
|
|
143
|
+
while (i < html.length) {
|
|
144
|
+
var spanStart = html.indexOf('<span', i);
|
|
145
|
+
if (spanStart === -1) { out += html.slice(i); break; }
|
|
146
|
+
var openEnd = html.indexOf('>', spanStart);
|
|
147
|
+
if (openEnd === -1) { out += html.slice(i); break; }
|
|
148
|
+
var openTag = html.slice(spanStart, openEnd + 1);
|
|
149
|
+
if (openTag.indexOf('context-value-inline') === -1) {
|
|
150
|
+
// Unrelated <span> — copy it through verbatim and keep scanning.
|
|
151
|
+
out += html.slice(i, openEnd + 1);
|
|
152
|
+
i = openEnd + 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Copy everything before the embed, then replace the whole embed span
|
|
156
|
+
// (including any nested inner spans) with the resolved value.
|
|
157
|
+
out += html.slice(i, spanStart);
|
|
158
|
+
var depth = 1;
|
|
159
|
+
var j = openEnd + 1;
|
|
160
|
+
while (j < html.length && depth > 0) {
|
|
161
|
+
var nextOpen = html.indexOf('<span', j);
|
|
162
|
+
var nextClose = html.indexOf('</span>', j);
|
|
163
|
+
if (nextClose === -1) { j = html.length; break; }
|
|
164
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
165
|
+
var innerEnd = html.indexOf('>', nextOpen);
|
|
166
|
+
j = innerEnd === -1 ? html.length : innerEnd + 1;
|
|
167
|
+
depth++;
|
|
168
|
+
} else {
|
|
169
|
+
j = nextClose + 7; // '</span>'.length
|
|
170
|
+
depth--;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
out += resolveRichTextContextSpan(openTag, context);
|
|
174
|
+
i = j;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveRichTextContextSpan(openTag, context) {
|
|
180
|
+
var nodeId = extractHtmlAttr(openTag, 'data-ctx-node-id');
|
|
181
|
+
var pathRaw = extractHtmlAttr(openTag, 'data-ctx-path');
|
|
182
|
+
var path = null;
|
|
183
|
+
if (pathRaw) {
|
|
184
|
+
var decoded = pathRaw.replace(/"/g, '"').replace(/"/g, '"').replace(/&/g, '&');
|
|
185
|
+
try { path = JSON.parse(decoded); } catch (e) { path = null; }
|
|
186
|
+
}
|
|
187
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
188
|
+
path = nodeId ? [nodeId] : [];
|
|
189
|
+
}
|
|
190
|
+
if (!nodeId && path.length > 0) { nodeId = path[0]; }
|
|
191
|
+
if (!nodeId) { return ''; }
|
|
192
|
+
var value = resolveContextRef({ type: 'workflowContext', nodeId: nodeId, path: path }, context);
|
|
193
|
+
if (value === undefined || value === null) { return ''; }
|
|
194
|
+
return escapeHtmlText(String(value));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Read a quoted HTML attribute value (double- or single-quoted) without a regex
|
|
198
|
+
// so the surrounding template literal needs no backslash escaping.
|
|
199
|
+
function extractHtmlAttr(tag, name) {
|
|
200
|
+
var key = name + '=';
|
|
201
|
+
var at = tag.indexOf(key);
|
|
202
|
+
if (at === -1) { return ''; }
|
|
203
|
+
var quote = tag.charAt(at + key.length);
|
|
204
|
+
if (quote !== '"' && quote !== "'") { return ''; }
|
|
205
|
+
var start = at + key.length + 1;
|
|
206
|
+
var end = tag.indexOf(quote, start);
|
|
207
|
+
if (end === -1) { return ''; }
|
|
208
|
+
return tag.slice(start, end);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function escapeHtmlText(value) {
|
|
212
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Scalar config values are normally just secret-resolved. Rich-text strings
|
|
216
|
+
// (email bodies) additionally need their inline context placeholders resolved.
|
|
217
|
+
function resolveScalarValue(value, context) {
|
|
218
|
+
if (typeof value === 'string' && value.indexOf('context-value-inline') !== -1) {
|
|
219
|
+
return resolveRichTextContext(value, context);
|
|
220
|
+
}
|
|
221
|
+
return resolveSecret(value, context);
|
|
222
|
+
}
|
|
223
|
+
|
|
128
224
|
function resolveConfig(config, context) {
|
|
129
225
|
if (!config || typeof config !== 'object') return config;
|
|
130
226
|
const resolved = {};
|
|
@@ -154,7 +250,7 @@ function resolveConfig(config, context) {
|
|
|
154
250
|
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
155
251
|
return resolveConfig(item, context);
|
|
156
252
|
}
|
|
157
|
-
return
|
|
253
|
+
return resolveScalarValue(item, context);
|
|
158
254
|
});
|
|
159
255
|
} else {
|
|
160
256
|
resolved[key] = val.map(function(item) {
|
|
@@ -164,7 +260,7 @@ function resolveConfig(config, context) {
|
|
|
164
260
|
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
165
261
|
return resolveConfig(item, context);
|
|
166
262
|
}
|
|
167
|
-
return
|
|
263
|
+
return resolveScalarValue(item, context);
|
|
168
264
|
});
|
|
169
265
|
}
|
|
170
266
|
} else if (val && typeof val === 'object' && val.type === 'workflowContext') {
|
|
@@ -176,7 +272,7 @@ function resolveConfig(config, context) {
|
|
|
176
272
|
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
177
273
|
resolved[key] = resolveConfig(val, context);
|
|
178
274
|
} else {
|
|
179
|
-
resolved[key] =
|
|
275
|
+
resolved[key] = resolveScalarValue(val, context);
|
|
180
276
|
}
|
|
181
277
|
}
|
|
182
278
|
return resolved;
|
|
@@ -741,6 +837,7 @@ module.exports = {
|
|
|
741
837
|
resolveSecret,
|
|
742
838
|
resolveConfig,
|
|
743
839
|
resolveContextRef,
|
|
840
|
+
resolveRichTextContext,
|
|
744
841
|
unwrapWorkflowCollection,
|
|
745
842
|
evaluateCondition,
|
|
746
843
|
evaluateSingleComparison,
|
|
@@ -244,6 +244,20 @@ async function general_custom_js(config: any, context: Record<string, unknown>)
|
|
|
244
244
|
const argValues: Record<string, unknown> = {
|
|
245
245
|
previousContext,
|
|
246
246
|
params,
|
|
247
|
+
// The workflow editor + AI generator advertise the top-level regular
|
|
248
|
+
// signature as `customHandler(params, inputs, workflowContext)` (see
|
|
249
|
+
// teleport-services-worker custom-js-contract.ts) where `workflowContext`
|
|
250
|
+
// is documented as "prior node results by node index" and `inputs` is a
|
|
251
|
+
// reserved positional. At top level that array IS `params` (built above in
|
|
252
|
+
// execution order: params[0] = trigger, params[1] = first node after the
|
|
253
|
+
// trigger). Without these aliases a handler that follows the advertised
|
|
254
|
+
// contract and reads `workflowContext[1]` gets `undefined[1]` → TypeError
|
|
255
|
+
// at runtime (the 2026-06-19 Sugarpost homepage "Something went wrong"
|
|
256
|
+
// page-load crash). Aliasing both to `params` honours the documented
|
|
257
|
+
// contract; the legacy `(params)` form is unaffected since it never names
|
|
258
|
+
// them, and inside a loop the still-distinct `innerParams*` win below.
|
|
259
|
+
inputs: params,
|
|
260
|
+
workflowContext: params,
|
|
247
261
|
}
|
|
248
262
|
for (let i = 0; i < innerParamsList.length; i++) {
|
|
249
263
|
const name = i === 0 ? 'innerParams' : 'innerParams' + (i + 1)
|