@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.
@@ -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
- if (pathname.startsWith(authRoutes[i])) {
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 NextResponse.redirect(new URL('/', request.url));
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
- matcher: ['/((?!api|_next/static|_next/image|favicon\\\\.ico).*)'],
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 &quot;-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(/&quot;/g, '"').replace(/&#34;/g, '"').replace(/&amp;/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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 resolveSecret(item, context);
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 resolveSecret(item, context);
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] = resolveSecret(val, context);
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)