@teleporthq/teleport-plugin-next-workflows 0.43.46 → 0.43.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/__tests__/auth-env-secret-preservation.test.ts +35 -0
  2. package/__tests__/auth-nextauth-url.test.ts +28 -0
  3. package/__tests__/auth-session-cost.test.ts +614 -0
  4. package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
  5. package/__tests__/data-node-not-awaited.test.ts +644 -0
  6. package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
  7. package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
  8. package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
  9. package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
  10. package/__tests__/raw-sql-param-binding.test.ts +82 -0
  11. package/__tests__/reserved-word-state-binding.test.ts +112 -0
  12. package/__tests__/runtime-stringified-boolean-truthiness.test.ts +76 -0
  13. package/__tests__/strip-event-handler-attrs.test.ts +59 -0
  14. package/__tests__/workflow-auth-guard.test.ts +277 -0
  15. package/dist/cjs/api-route-generator.d.ts +3 -3
  16. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  17. package/dist/cjs/api-route-generator.js +136 -11
  18. package/dist/cjs/api-route-generator.js.map +1 -1
  19. package/dist/cjs/auth-generator.d.ts +67 -1
  20. package/dist/cjs/auth-generator.d.ts.map +1 -1
  21. package/dist/cjs/auth-generator.js +458 -20
  22. package/dist/cjs/auth-generator.js.map +1 -1
  23. package/dist/cjs/await-result.d.ts +31 -0
  24. package/dist/cjs/await-result.d.ts.map +1 -0
  25. package/dist/cjs/await-result.js +50 -0
  26. package/dist/cjs/await-result.js.map +1 -0
  27. package/dist/cjs/executor-generator.d.ts.map +1 -1
  28. package/dist/cjs/executor-generator.js +162 -3
  29. package/dist/cjs/executor-generator.js.map +1 -1
  30. package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
  31. package/dist/cjs/nodes/account/account-get-current.js +44 -11
  32. package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
  33. package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
  34. package/dist/cjs/nodes/account/account-login.js +10 -1
  35. package/dist/cjs/nodes/account/account-login.js.map +1 -1
  36. package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
  37. package/dist/cjs/raw-sql-param-binding.js +32 -0
  38. package/dist/cjs/raw-sql-param-binding.js.map +1 -1
  39. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  40. package/dist/cjs/segment-splitter.js +15 -7
  41. package/dist/cjs/segment-splitter.js.map +1 -1
  42. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  43. package/dist/cjs/workflow-auth-generator.d.ts +26 -0
  44. package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
  45. package/dist/cjs/workflow-auth-generator.js +166 -0
  46. package/dist/cjs/workflow-auth-generator.js.map +1 -0
  47. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  48. package/dist/cjs/workflow-component-plugin.js +107 -16
  49. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  50. package/dist/cjs/workflow-project-plugin.d.ts +2 -1
  51. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  52. package/dist/cjs/workflow-project-plugin.js +170 -20
  53. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  54. package/dist/esm/api-route-generator.d.ts +3 -3
  55. package/dist/esm/api-route-generator.d.ts.map +1 -1
  56. package/dist/esm/api-route-generator.js +136 -11
  57. package/dist/esm/api-route-generator.js.map +1 -1
  58. package/dist/esm/auth-generator.d.ts +67 -1
  59. package/dist/esm/auth-generator.d.ts.map +1 -1
  60. package/dist/esm/auth-generator.js +453 -19
  61. package/dist/esm/auth-generator.js.map +1 -1
  62. package/dist/esm/await-result.d.ts +31 -0
  63. package/dist/esm/await-result.d.ts.map +1 -0
  64. package/dist/esm/await-result.js +45 -0
  65. package/dist/esm/await-result.js.map +1 -0
  66. package/dist/esm/executor-generator.d.ts.map +1 -1
  67. package/dist/esm/executor-generator.js +162 -3
  68. package/dist/esm/executor-generator.js.map +1 -1
  69. package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
  70. package/dist/esm/nodes/account/account-get-current.js +44 -11
  71. package/dist/esm/nodes/account/account-get-current.js.map +1 -1
  72. package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
  73. package/dist/esm/nodes/account/account-login.js +10 -1
  74. package/dist/esm/nodes/account/account-login.js.map +1 -1
  75. package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
  76. package/dist/esm/raw-sql-param-binding.js +32 -0
  77. package/dist/esm/raw-sql-param-binding.js.map +1 -1
  78. package/dist/esm/segment-splitter.d.ts.map +1 -1
  79. package/dist/esm/segment-splitter.js +15 -7
  80. package/dist/esm/segment-splitter.js.map +1 -1
  81. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  82. package/dist/esm/workflow-auth-generator.d.ts +26 -0
  83. package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
  84. package/dist/esm/workflow-auth-generator.js +161 -0
  85. package/dist/esm/workflow-auth-generator.js.map +1 -0
  86. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  87. package/dist/esm/workflow-component-plugin.js +108 -17
  88. package/dist/esm/workflow-component-plugin.js.map +1 -1
  89. package/dist/esm/workflow-project-plugin.d.ts +2 -1
  90. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  91. package/dist/esm/workflow-project-plugin.js +169 -20
  92. package/dist/esm/workflow-project-plugin.js.map +1 -1
  93. package/package.json +4 -4
  94. package/src/api-route-generator.ts +139 -11
  95. package/src/auth-generator.ts +471 -19
  96. package/src/await-result.ts +52 -0
  97. package/src/executor-generator.ts +162 -3
  98. package/src/nodes/account/account-get-current.ts +44 -9
  99. package/src/nodes/account/account-login.ts +12 -1
  100. package/src/raw-sql-param-binding.ts +33 -0
  101. package/src/segment-splitter.ts +15 -7
  102. package/src/workflow-auth-generator.ts +187 -0
  103. package/src/workflow-component-plugin.ts +129 -20
  104. package/src/workflow-project-plugin.ts +186 -19
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateSessionProviderWrapper = exports.generateMiddlewareFile = exports.generateSignupRouteFile = exports.generateNextAuthRouteFile = exports.generateHashPasswordFile = exports.generateAuthOptionsFile = exports.getDatabaseDriverDependencies = void 0;
3
+ exports.generateSessionProviderWrapper = exports.generateNextAuthUrlGuardModule = exports.generateMiddlewareFile = exports.generateSignupRouteFile = exports.generateNextAuthRouteFile = exports.generateHashPasswordFile = exports.generateAuthDbHealthFile = exports.generateAuthErrorMessagesFile = exports.generateAuthOptionsFile = exports.buildSessionUserFields = exports.SENSITIVE_USER_FIELDS = exports.SESSION_SAFE_USER_FIELDS = exports.USER_REFRESH_INTERVAL_MS = exports.getDatabaseDriverDependencies = void 0;
4
4
  const transactional_email_code_1 = require("./transactional-email-code");
5
5
  // GUI provider id → the actual next-auth v4 provider MODULE name, for the few
6
6
  // cases where they differ. Everything else uses the id as the module name.
@@ -167,19 +167,110 @@ const airtableBase = new Airtable({
167
167
  return '';
168
168
  }
169
169
  };
170
- const generateSanitizeUserFunction = () => {
170
+ /**
171
+ * How long (ms) a JWT may serve the profile fields it already carries before
172
+ * the next session request re-reads the `users` row. See the generated
173
+ * comment in `generateAuthOptionsFile` for the measurements behind it.
174
+ */
175
+ exports.USER_REFRESH_INTERVAL_MS = 60000;
176
+ /**
177
+ * Profile fields the session may always carry, even for a UIDL that ships no
178
+ * `users` schema at all.
179
+ *
180
+ * `id`/`name`/`email`/`image`/`role` are the fields the `account-get-current`
181
+ * output contract declares (workflow-schema's node-context-schemas), i.e. the
182
+ * ones every workflow and binding addresses. `roleName`/`roles` are the two
183
+ * alternate spellings `getUserRoleFromToken` in the generated middleware falls
184
+ * back to, so dropping them would silently disable role-based route protection
185
+ * for projects that use them.
186
+ */
187
+ exports.SESSION_SAFE_USER_FIELDS = [
188
+ 'id',
189
+ 'name',
190
+ 'email',
191
+ 'image',
192
+ 'role',
193
+ 'roleName',
194
+ 'roles',
195
+ ];
196
+ /**
197
+ * Credentials that must never leave the server, whatever the `users` table
198
+ * happens to declare: the OAuth single-table adapter's provider tokens plus the
199
+ * password hash. `findUserByEmail` does `SELECT *`, so without this list they
200
+ * ride the JWT into `session.user` and become readable by any script on the
201
+ * page through /api/auth/session.
202
+ *
203
+ * Denied unconditionally, so a custom user property whose key collides with one
204
+ * of these names cannot re-open the hole. `provider` itself is NOT here — it is
205
+ * just the provider's name ("google"), and `account-social-login` declares it
206
+ * as part of its output contract.
207
+ */
208
+ exports.SENSITIVE_USER_FIELDS = [
209
+ 'password',
210
+ 'password_hash',
211
+ 'passwordHash',
212
+ 'access_token',
213
+ 'refresh_token',
214
+ 'id_token',
215
+ 'expires_at',
216
+ 'session_state',
217
+ 'scope',
218
+ 'token_type',
219
+ 'provider_account_id',
220
+ 'provider_type',
221
+ ];
222
+ /**
223
+ * Mongo's primary key. NOT a secret — it is excluded because `sanitizeUser`
224
+ * already folds it into `id` (`rawId = user.id != null ? user.id : user._id`),
225
+ * so passing it through as well would put the same value on the session twice
226
+ * under two different names. A shape concern, not a security one, which is why
227
+ * it is kept out of `SENSITIVE_USER_FIELDS`.
228
+ */
229
+ const ID_ALIAS_FIELD = '_id';
230
+ /**
231
+ * The exact set of `users` columns this project allows into the session.
232
+ *
233
+ * Derived from the DECLARED schema (`auth.tables.users`), which the GUI
234
+ * composes as the canonical auth columns PLUS one column per custom account
235
+ * property — so every property the user configured is included by
236
+ * construction, while a column that exists in the database but not in the UIDL
237
+ * never is. `customUserProperties` is unioned in directly rather than relied on
238
+ * transitively, so the list stays complete for a UIDL that carries the
239
+ * properties without a matching `tables` entry.
240
+ */
241
+ const buildSessionUserFields = (tables, customProps) => {
242
+ const declared = ((tables === null || tables === void 0 ? void 0 : tables.users) || []).map((column) => column.name);
243
+ const custom = customProps.map((prop) => prop.key);
244
+ const denied = new Set(exports.SENSITIVE_USER_FIELDS);
245
+ return Array.from(new Set([...exports.SESSION_SAFE_USER_FIELDS, ...declared, ...custom])).filter((field) => Boolean(field) && field !== ID_ALIAS_FIELD && !denied.has(field));
246
+ };
247
+ exports.buildSessionUserFields = buildSessionUserFields;
248
+ const generateSanitizeUserFunction = (tables, customProps) => {
249
+ // ALLOW-list, not a deny-list. `sanitizeUser` feeds the JWT, and the session
250
+ // callback copies the whole token onto `session.user`, so every column the
251
+ // `SELECT *` returned used to be readable by any script on the page. A
252
+ // two-entry deny-list (`password`, `_id`) could never keep up with a table
253
+ // whose shape the project controls; enumerating what the project actually
254
+ // DECLARED is the version that stays correct as columns are added.
255
+ //
171
256
  // Preserve the native type of the id (integer vs UUID-string) — NextAuth
172
257
  // will JSON-serialize the session payload either way. Coercing with
173
258
  // \`String(rawId)\` would silently turn an integer PK into "1" and make
174
259
  // runtime comparisons against a DB-fetched \`user.id\` (still a number
175
260
  // on pages that read the row via \`getStaticProps\`) always false.
176
- return `function sanitizeUser(user) {
261
+ return `// The only \`users\` columns that may reach the browser: this project's declared
262
+ // user schema — canonical profile columns plus every custom account property —
263
+ // minus the credentials (password hash, OAuth access/refresh/id tokens), which
264
+ // stay server-side. Anything else the row carries never reaches the session.
265
+ const SESSION_USER_FIELDS = ${JSON.stringify((0, exports.buildSessionUserFields)(tables, customProps))};
266
+
267
+ function sanitizeUser(user) {
177
268
  if (!user) return null;
178
269
  const safe = {};
179
- const keys = Object.keys(user);
180
- for (let i = 0; i < keys.length; i++) {
181
- if (keys[i] !== 'password' && keys[i] !== '_id') {
182
- safe[keys[i]] = user[keys[i]];
270
+ for (let i = 0; i < SESSION_USER_FIELDS.length; i++) {
271
+ const key = SESSION_USER_FIELDS[i];
272
+ if (user[key] !== undefined) {
273
+ safe[key] = user[key];
183
274
  }
184
275
  }
185
276
  const rawId = user.id != null ? user.id : user._id;
@@ -489,19 +580,46 @@ const generateProvidersSetup = (auth) => {
489
580
  email: { label: 'Email', type: 'email' },
490
581
  password: { label: 'Password', type: 'password' }
491
582
  },
583
+ // Every failure used to \`return null\`, which NextAuth reports as the single
584
+ // opaque code \`CredentialsSignin\` — the same answer for a wrong password, an
585
+ // unknown email, and a database the app cannot reach. Project a62338f9 shipped
586
+ // with a DB credential Postgres rejected, and the only thing the user (or the
587
+ // sign-in form) ever saw was "credentialsSignin".
588
+ //
589
+ // NextAuth v4 surfaces a THROWN error's message as the \`error\` value, so each
590
+ // cause now gets its own code. \`describeAuthError\` in
591
+ // utils/auth/auth-error-messages turns them into copy; the raw cause is logged
592
+ // server-side only.
593
+ //
594
+ // Unknown email and wrong password deliberately share one code: telling them
595
+ // apart is a user-enumeration oracle.
492
596
  async authorize(credentials) {
493
597
  if (!credentials || !credentials.email || !credentials.password) {
494
- return null;
598
+ throw new Error('MissingCredentials');
495
599
  }
600
+ const { verifyPassword } = require('./hash-password');
601
+ const { classifyAuthInfrastructureError } = require('./db-health');
602
+ let user;
496
603
  try {
497
- const { verifyPassword } = require('./hash-password');
498
- const user = await findUserByEmail(String(credentials.email));
499
- if (!user) return null;
500
- if (!verifyPassword(String(credentials.password), user.password)) return null;
604
+ user = await findUserByEmail(String(credentials.email));
605
+ } catch (err) {
606
+ const kind = classifyAuthInfrastructureError(err);
607
+ console.error('[auth] Could not reach the user store (' + kind + '):', err && err.message ? err.message : err);
608
+ throw new Error('ServiceUnavailable');
609
+ }
610
+ try {
611
+ if (!user) throw new Error('InvalidCredentials');
612
+ if (!verifyPassword(String(credentials.password), user.password)) {
613
+ throw new Error('InvalidCredentials');
614
+ }
501
615
  return sanitizeUser(user);
502
616
  } catch (err) {
503
- console.error('Authorize error:', err);
504
- return null;
617
+ if (err && err.message === 'InvalidCredentials') throw err;
618
+ // A hashing/serialisation fault is an infrastructure problem, not a
619
+ // wrong password — saying "check your details" would send the user
620
+ // round in circles.
621
+ console.error('[auth] Credential check failed:', err && err.message ? err.message : err);
622
+ throw new Error('ServiceUnavailable');
505
623
  }
506
624
  }
507
625
  }));`);
@@ -539,7 +657,7 @@ const generateAuthOptionsFile = (auth, dataSourceConfig) => {
539
657
  const findUserCode = auth.passwordAuthEnabled
540
658
  ? generateFindUserFunction(auth.dataSourceType, customProps)
541
659
  : '';
542
- const sanitizeUserCode = generateSanitizeUserFunction();
660
+ const sanitizeUserCode = generateSanitizeUserFunction(auth.tables, customProps);
543
661
  const signInRoute = ((_a = auth.authPages.signIn) === null || _a === void 0 ? void 0 : _a.route) || '/auth/sign-in';
544
662
  return `${providerImports}
545
663
  ${dbSetupCode}
@@ -549,6 +667,23 @@ ${findUserCode}
549
667
 
550
668
  ${providersSetup}
551
669
 
670
+ // How long a JWT may serve profile fields before the next /api/auth/session
671
+ // request re-reads the \`users\` row. \`strategy: 'jwt'\` exists precisely so a
672
+ // session costs no database round trip; refreshing on EVERY request threw that
673
+ // away — each one opened a fresh unpooled pg connection (TCP + TLS handshake)
674
+ // for a single indexed SELECT, which measured ~715ms of the ~925ms that
675
+ // /api/auth/session took on a published Vercel deployment, against ~210ms for
676
+ // the same route when the token carried no email. Profile edits still land
677
+ // immediately whenever the app calls \`useSession().update()\` (NextAuth passes
678
+ // \`trigger === 'update'\`, which bypasses the interval); the interval is the
679
+ // backstop for changes made outside this browser, e.g. an admin editing a role.
680
+ const USER_REFRESH_INTERVAL_MS = ${exports.USER_REFRESH_INTERVAL_MS};
681
+
682
+ // Bookkeeping the refresh policy writes onto the token. It must never be copied
683
+ // onto \`session.user\` — it is not a profile field, and leaking it would put an
684
+ // internal timestamp on every binding that enumerates the user object.
685
+ const TOKEN_REFRESH_STAMP = '__userRefreshedAt';
686
+
552
687
  const authOptions = {
553
688
  providers: providers,
554
689
  pages: {
@@ -564,6 +699,13 @@ const authOptions = {
564
699
  for (let i = 0; i < keys.length; i++) {
565
700
  token[keys[i]] = user[keys[i]];
566
701
  }
702
+ // Deliberately NOT stamped. An OAuth \`user\` is the PROVIDER's profile
703
+ // (id/name/email/image) — it carries no \`role\`, which only exists on
704
+ // the \`users\` row. Stamping here would make the first session request
705
+ // after an OAuth sign-in skip the database and leave the visitor
706
+ // role-less for a whole interval, silently failing role-protected
707
+ // routes. Leaving it unstamped means the next session request refreshes
708
+ // exactly as it does today, and the interval applies from there on.
567
709
  return token;
568
710
  }
569
711
  // Subsequent calls (every /api/auth/session): re-read the user from the
@@ -571,6 +713,22 @@ const authOptions = {
571
713
  // session. The JWT is otherwise a snapshot captured at login, which is why
572
714
  // the navbar avatar/name reverted to the old value after a refresh: the
573
715
  // navbar reads /api/auth/session, which is derived from this token.
716
+ //
717
+ // Rate-limited to USER_REFRESH_INTERVAL_MS, because that read is by far
718
+ // the most expensive thing a session request does. \`trigger === 'update'\`
719
+ // (fired by \`useSession().update()\`, which the auth bridge re-publishes
720
+ // as \`window.__teleportNextAuth.refreshSession\`) always refreshes, so a
721
+ // profile save is reflected at once rather than up to an interval later.
722
+ const now = Date.now();
723
+ const lastRefresh = token && typeof token[TOKEN_REFRESH_STAMP] === 'number'
724
+ ? token[TOKEN_REFRESH_STAMP]
725
+ : 0;
726
+ // \`now - lastRefresh < 0\` covers a clock that moved backwards: treat the
727
+ // stamp as stale rather than trusting it until the clock catches up.
728
+ const isFresh = now - lastRefresh >= 0 && now - lastRefresh < USER_REFRESH_INTERVAL_MS;
729
+ if (params.trigger !== 'update' && isFresh) {
730
+ return token;
731
+ }
574
732
  try {
575
733
  if (token && token.email && typeof findUserByEmail === 'function') {
576
734
  const fresh = await findUserByEmail(String(token.email));
@@ -585,6 +743,11 @@ const authOptions = {
585
743
  } catch (e) {
586
744
  // Keep the existing token on any DB hiccup — never sign the user out.
587
745
  }
746
+ // Stamped even when the read threw, so an unreachable database costs one
747
+ // attempt per interval instead of one per request.
748
+ if (token) {
749
+ token[TOKEN_REFRESH_STAMP] = now;
750
+ }
588
751
  return token;
589
752
  },
590
753
  async session(params) {
@@ -592,6 +755,7 @@ const authOptions = {
592
755
  const token = params.token;
593
756
  if (token && session.user) {
594
757
  const skip = { iat: 1, exp: 1, jti: 1, sub: 1 };
758
+ skip[TOKEN_REFRESH_STAMP] = 1;
595
759
  const keys = Object.keys(token);
596
760
  for (let i = 0; i < keys.length; i++) {
597
761
  if (!skip[keys[i]]) {
@@ -616,6 +780,125 @@ ${auth.passwordAuthEnabled
616
780
  `;
617
781
  };
618
782
  exports.generateAuthOptionsFile = generateAuthOptionsFile;
783
+ /**
784
+ * Why a sign-in failed, in words, plus the one-line codes `authorize` throws.
785
+ *
786
+ * Project a62338f9 could not sign in on the published site OR in the exported
787
+ * project, and the only signal anywhere was NextAuth's generic
788
+ * `CredentialsSignin`. The real cause was that Postgres rejected the DB
789
+ * credential baked into `TELEPORT_DB_CONNECTION_STRING` — a configuration
790
+ * failure the form reported as if the user had mistyped their password.
791
+ *
792
+ * Every code NextAuth can produce is mapped, not just the new ones, so the form
793
+ * never shows a machine token again.
794
+ */
795
+ const generateAuthErrorMessagesFile = () => {
796
+ return `// Codes thrown by the credentials provider's authorize().
797
+ const AUTH_ERROR_MESSAGES = {
798
+ // Ours.
799
+ MissingCredentials: 'Please enter both your email and your password.',
800
+ InvalidCredentials: 'That email and password do not match an account.',
801
+ ServiceUnavailable:
802
+ 'We could not reach the account service. Please try again in a moment — if it keeps happening, the site owner needs to check its database configuration.',
803
+
804
+ // NextAuth's own.
805
+ CredentialsSignin: 'That email and password do not match an account.',
806
+ SessionRequired: 'Please sign in to continue.',
807
+ AccessDenied: 'This account is not allowed to sign in.',
808
+ Verification: 'That sign-in link is no longer valid. Please request a new one.',
809
+ Configuration:
810
+ 'Sign-in is not configured correctly for this site. The site owner needs to check its authentication settings.',
811
+ OAuthSignin: 'We could not start sign-in with that provider. Please try again.',
812
+ OAuthCallback: 'That provider could not complete sign-in. Please try again.',
813
+ OAuthCreateAccount: 'We could not create an account from that provider.',
814
+ OAuthAccountNotLinked:
815
+ 'An account with this email already exists. Sign in the way you did originally, then link this provider from your profile.',
816
+ EmailCreateAccount: 'We could not create an account with that email address.',
817
+ EmailSignin: 'We could not send the sign-in email. Please try again.',
818
+ Callback: 'Sign-in could not be completed. Please try again.',
819
+ Default: 'Something went wrong while signing in. Please try again.',
820
+ };
821
+
822
+ /**
823
+ * Human copy for a NextAuth error code. Anything unrecognised — including a
824
+ * message a future provider invents — falls back to the generic line rather
825
+ * than being shown raw.
826
+ */
827
+ function describeAuthError(code) {
828
+ if (!code) { return ''; }
829
+ var key = String(code);
830
+ if (Object.prototype.hasOwnProperty.call(AUTH_ERROR_MESSAGES, key)) {
831
+ return AUTH_ERROR_MESSAGES[key];
832
+ }
833
+ return AUTH_ERROR_MESSAGES.Default;
834
+ }
835
+
836
+ module.exports = describeAuthError;
837
+ module.exports.describeAuthError = describeAuthError;
838
+ module.exports.AUTH_ERROR_MESSAGES = AUTH_ERROR_MESSAGES;
839
+ `;
840
+ };
841
+ exports.generateAuthErrorMessagesFile = generateAuthErrorMessagesFile;
842
+ /**
843
+ * Tells a database that is UNREACHABLE from one that REFUSED the credential.
844
+ *
845
+ * Both surface to the user as "we could not reach the account service", but the
846
+ * server log has to name which one: run a62338f9's published site and its
847
+ * exported project both failed with `password authentication failed for user
848
+ * "p_<project>_usr"`, and nothing in the app ever said so.
849
+ */
850
+ const generateAuthDbHealthFile = () => {
851
+ return `var AUTH_DB_ERROR_KINDS = {
852
+ AUTH_FAILED: 'db-auth-failed',
853
+ UNREACHABLE: 'db-unreachable',
854
+ MISCONFIGURED: 'db-misconfigured',
855
+ UNKNOWN: 'db-unknown',
856
+ };
857
+
858
+ /** Postgres SQLSTATEs for "the server said no to these credentials". */
859
+ var AUTH_REJECTION_CODES = ['28P01', '28000', '3D000'];
860
+
861
+ function classifyAuthInfrastructureError(err) {
862
+ if (!err) { return AUTH_DB_ERROR_KINDS.UNKNOWN; }
863
+ var code = err.code ? String(err.code) : '';
864
+ var message = err.message ? String(err.message).toLowerCase() : '';
865
+ if (AUTH_REJECTION_CODES.indexOf(code) !== -1 || message.indexOf('password authentication failed') !== -1) {
866
+ return AUTH_DB_ERROR_KINDS.AUTH_FAILED;
867
+ }
868
+ if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'EAI_AGAIN') {
869
+ return AUTH_DB_ERROR_KINDS.UNREACHABLE;
870
+ }
871
+ if (message.indexOf('connection string') !== -1 || message.indexOf('client password must be a string') !== -1) {
872
+ return AUTH_DB_ERROR_KINDS.MISCONFIGURED;
873
+ }
874
+ return AUTH_DB_ERROR_KINDS.UNKNOWN;
875
+ }
876
+
877
+ /**
878
+ * True when a value is still the build-time placeholder the deploy step was
879
+ * supposed to replace. A placeholder is WORSE than an empty value: code that
880
+ * treats "set" as "configured" then trusts a string that means nothing.
881
+ */
882
+ function isUnresolvedSecretPlaceholder(value) {
883
+ return typeof value === 'string' && value.indexOf('teleporthq.secrets.') === 0;
884
+ }
885
+
886
+ /** \`NEXTAUTH_URL\` is only usable when it is an absolute http(s) origin. */
887
+ function isUsableNextAuthUrl(value) {
888
+ if (typeof value !== 'string' || !value) { return false; }
889
+ if (isUnresolvedSecretPlaceholder(value)) { return false; }
890
+ return /^https?:\\/\\//i.test(value);
891
+ }
892
+
893
+ module.exports = {
894
+ AUTH_DB_ERROR_KINDS: AUTH_DB_ERROR_KINDS,
895
+ classifyAuthInfrastructureError: classifyAuthInfrastructureError,
896
+ isUnresolvedSecretPlaceholder: isUnresolvedSecretPlaceholder,
897
+ isUsableNextAuthUrl: isUsableNextAuthUrl,
898
+ };
899
+ `;
900
+ };
901
+ exports.generateAuthDbHealthFile = generateAuthDbHealthFile;
619
902
  const generateHashPasswordFile = () => {
620
903
  return `const bcrypt = require('bcryptjs');
621
904
  const crypto = require('crypto');
@@ -653,24 +936,54 @@ const authOptions = require('../../../utils/auth/auth-options');
653
936
 
654
937
  const authHandler = NextAuth(authOptions);
655
938
 
939
+ // Inlined rather than required from utils/auth/db-health: this route is the
940
+ // entry point for ALL authentication, so it must not depend on a sibling module
941
+ // resolving. Three lines, and the shared copy still backs authorize().
942
+ function isUsableNextAuthUrl(value) {
943
+ if (typeof value !== 'string' || !value) { return false; }
944
+ if (value.indexOf('teleporthq.secrets.') === 0) { return false; }
945
+ return /^https?:\\/\\//i.test(value);
946
+ }
947
+
656
948
  // NextAuth builds OAuth callback / redirect URLs from process.env.NEXTAUTH_URL.
657
949
  // At build time the real deployment domain is unknown, so the generated .env
658
950
  // ships a localhost default. Derive the correct origin from the incoming
659
951
  // request here so OAuth works on whatever domain the project is published to —
660
952
  // WITHOUT hardcoding it. An explicitly-configured (non-local) NEXTAUTH_URL is
661
953
  // always respected; local dev (host = localhost) is left untouched.
954
+ //
955
+ // "Explicitly configured" means a REAL absolute http(s) origin. Project
956
+ // a62338f9 shipped \`NEXTAUTH_URL=teleporthq.secrets.NEXTAUTH_URL\` — an
957
+ // unresolved placeholder — and the old check saw a non-empty, non-localhost
958
+ // string and refused to override it. NextAuth then normalised it to
959
+ // \`https://teleporthq.secrets.nextauth_url\`, advertised that as the sign-in
960
+ // origin and issued \`__Host-\`/\`__Secure-\` cookies against it. Treating anything
961
+ // that is not a usable URL as unset lets an already-published site self-heal on
962
+ // its next request.
662
963
  module.exports = function nextAuthRoute(req, res) {
663
964
  try {
664
965
  const fwdHost = req.headers['x-forwarded-host'] || req.headers.host || '';
665
966
  const host = Array.isArray(fwdHost) ? fwdHost[0] : fwdHost;
666
967
  const hostIsLocal = host.indexOf('localhost') === 0 || host.indexOf('127.0.0.1') === 0;
667
968
  const current = process.env.NEXTAUTH_URL || '';
969
+ if (current && !isUsableNextAuthUrl(current)) {
970
+ console.error(
971
+ '[auth] NEXTAUTH_URL is not a usable origin (' + current + ') — deriving it from the request instead.'
972
+ );
973
+ }
668
974
  const currentIsLocalOrEmpty =
669
- !current || current.indexOf('localhost') !== -1 || current.indexOf('127.0.0.1') !== -1;
975
+ !isUsableNextAuthUrl(current) ||
976
+ current.indexOf('localhost') !== -1 ||
977
+ current.indexOf('127.0.0.1') !== -1;
670
978
  if (host && !hostIsLocal && currentIsLocalOrEmpty) {
671
979
  const fwdProto = req.headers['x-forwarded-proto'];
672
980
  const proto = (Array.isArray(fwdProto) ? fwdProto[0] : fwdProto) || 'https';
673
981
  process.env.NEXTAUTH_URL = proto + '://' + host;
982
+ } else if (hostIsLocal && !isUsableNextAuthUrl(current)) {
983
+ // Local dev with an unusable value would otherwise leave NextAuth
984
+ // deriving \`https://…\` from the placeholder, so its cookies get the
985
+ // \`Secure\` prefix and the browser drops them over plain http.
986
+ process.env.NEXTAUTH_URL = 'http://' + (host || 'localhost:3000');
674
987
  }
675
988
  } catch (e) {
676
989
  /* fall back to the configured NEXTAUTH_URL */
@@ -1176,6 +1489,55 @@ export const config = {
1176
1489
  `;
1177
1490
  };
1178
1491
  exports.generateMiddlewareFile = generateMiddlewareFile;
1492
+ // A side-effect-only module that MUST evaluate before `next-auth/react` is
1493
+ // imported. It is emitted at utils/auth/nextauth-url-guard.js and imported as the
1494
+ // FIRST import of session-provider.js (the only module that imports
1495
+ // next-auth/react). ES module imports evaluate in source order, so this runs
1496
+ // first.
1497
+ //
1498
+ // Why it exists: next-auth v4's `next-auth/react` builds its `__NEXTAUTH` config
1499
+ // at MODULE LOAD by calling `parseUrl(process.env.NEXTAUTH_URL)` (and the
1500
+ // NEXTAUTH_URL_INTERNAL / VERCEL_URL variants). `parseUrl('')` runs `new URL('')`,
1501
+ // which throws "Invalid URL" — so an EMPTY-STRING env value crashes the import
1502
+ // and, with it, server-side rendering for every page that mounts SessionProvider
1503
+ // (the terminal shows `TypeError: Invalid URL … input: '' … page: '/products'`).
1504
+ // An UNSET var is safe: next-auth defaults it to http://localhost:3000. But a
1505
+ // generated `.env` can ship `NEXTAUTH_URL=` (empty), which Next.js loads into
1506
+ // process.env as "" rather than undefined — the crashing case. Deleting an
1507
+ // empty/whitespace value restores the safe "unset" state; a real configured
1508
+ // value is left untouched. On the browser these are already undefined, so this
1509
+ // is a no-op there.
1510
+ const generateNextAuthUrlGuardModule = () => {
1511
+ return `// GENERATED — see generateNextAuthUrlGuardModule in
1512
+ // @teleporthq/teleport-plugin-next-workflows/src/auth-generator.ts.
1513
+ //
1514
+ // next-auth v4's \`next-auth/react\` reads \`parseUrl(process.env.NEXTAUTH_URL)\` at
1515
+ // MODULE LOAD. \`new URL('')\` throws "Invalid URL", so an EMPTY-STRING value
1516
+ // crashes the import — and server-side rendering for every page that mounts
1517
+ // SessionProvider. An UNSET var is fine (next-auth defaults it to
1518
+ // http://localhost:3000); only an empty string is fatal. A generated \`.env\` may
1519
+ // ship \`NEXTAUTH_URL=\` (empty), which Next.js loads as "", not undefined.
1520
+ //
1521
+ // This is the FIRST import of session-provider.js — the only module that imports
1522
+ // next-auth/react — and ES module imports evaluate in source order, so this runs
1523
+ // before next-auth reads the value. It removes an empty / whitespace value from
1524
+ // the server's process.env so next-auth falls back to its default. On the client
1525
+ // these are already undefined, so it is a no-op.
1526
+ if (typeof process !== 'undefined' && process && process.env) {
1527
+ var __TQ_NEXTAUTH_URL_KEYS = ['NEXTAUTH_URL', 'NEXTAUTH_URL_INTERNAL', 'VERCEL_URL']
1528
+ for (var __i = 0; __i < __TQ_NEXTAUTH_URL_KEYS.length; __i++) {
1529
+ var __k = __TQ_NEXTAUTH_URL_KEYS[__i]
1530
+ var __v = process.env[__k]
1531
+ if (typeof __v === 'string' && __v.trim() === '') {
1532
+ delete process.env[__k]
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ export {}
1538
+ `;
1539
+ };
1540
+ exports.generateNextAuthUrlGuardModule = generateNextAuthUrlGuardModule;
1179
1541
  const generateSessionProviderWrapper = () => {
1180
1542
  // Authored as pure ESM. _app.js imports this with an ESM default import
1181
1543
  // (`import AuthSessionProvider from '.../session-provider'`). When the file
@@ -1187,8 +1549,15 @@ const generateSessionProviderWrapper = () => {
1187
1549
  // it worked locally). Keeping a single, consistent module system removes the
1188
1550
  // fragile boundary. `SessionProvider` is imported by name (the SWC-safe form
1189
1551
  // even though next-auth/react ships CommonJS).
1190
- return `import React from 'react'
1191
- import { SessionProvider, signIn, signOut } from 'next-auth/react'
1552
+ //
1553
+ // The `nextauth-url-guard` import MUST stay first: it normalizes an empty
1554
+ // NEXTAUTH_URL out of process.env before `next-auth/react` evaluates and calls
1555
+ // `parseUrl('')`, which would otherwise throw "Invalid URL" at module load and
1556
+ // crash SSR. ES imports run in source order, so first = before next-auth/react.
1557
+ return `import './nextauth-url-guard'
1558
+ import React from 'react'
1559
+ import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react'
1560
+ import describeAuthError from './auth-error-messages'
1192
1561
 
1193
1562
  // Bridge for the workflow account handlers (account-login / -logout / -signup /
1194
1563
  // -social-login). Those handlers are emitted via fn.toString() and re-bundled
@@ -1201,14 +1570,83 @@ import { SessionProvider, signIn, signOut } from 'next-auth/react'
1201
1570
  // next-auth/react that is reliably bundled into the app shell) guarantees the
1202
1571
  // handlers can read them off \`window\` without any fragile require/import of
1203
1572
  // their own. The assignment runs at module-eval time, before any click.
1573
+ // \`describeAuthError\` rides the same bridge for the same reason. The sign-in
1574
+ // handler used to surface NextAuth's raw code — project a62338f9's users were
1575
+ // shown the literal string "credentialsSignin" — and it cannot require the
1576
+ // message table itself without reintroducing exactly the fragile require this
1577
+ // bridge exists to avoid. Published from here, where a normal module import is
1578
+ // safe, so there is ONE table rather than a copy inlined per handler.
1579
+ // A stable object, assigned to \`window\` exactly once at module-eval time.
1580
+ // \`SessionSnapshotBridge\` below mutates it in place, so a handler that grabbed
1581
+ // \`window.__teleportNextAuth\` earlier always observes the current session.
1582
+ const teleportNextAuth = {
1583
+ signIn: signIn,
1584
+ signOut: signOut,
1585
+ describeAuthError: describeAuthError,
1586
+ // The session SessionProvider is ALREADY holding in memory, republished so
1587
+ // that workflow handlers can read it synchronously instead of paying an HTTP
1588
+ // round trip to /api/auth/session for something the page has had since it
1589
+ // mounted. On a published deployment that request measured ~925ms — the
1590
+ // \`account-get-current\` node fired one on every click that resolves the
1591
+ // current user (favourites, cart, reviews, ...), strictly serial with the
1592
+ // workflow's own request. \`status\` is next-auth's: 'loading' until the
1593
+ // provider's first fetch settles, then 'authenticated' / 'unauthenticated'.
1594
+ session: null,
1595
+ status: 'loading',
1596
+ getSession: function () {
1597
+ return { status: teleportNextAuth.status, session: teleportNextAuth.session }
1598
+ },
1599
+ // \`useSession().update()\`. Calling it re-reads the user server-side (NextAuth
1600
+ // passes \`trigger: 'update'\` to the jwt callback, which bypasses the refresh
1601
+ // interval) and updates the in-memory session. This is how a profile save
1602
+ // makes its change visible immediately rather than at the next interval.
1603
+ refreshSession: function () {
1604
+ return Promise.resolve(null)
1605
+ },
1606
+ }
1607
+
1204
1608
  if (typeof window !== 'undefined') {
1205
- window.__teleportNextAuth = { signIn: signIn, signOut: signOut }
1609
+ window.__teleportNextAuth = teleportNextAuth
1610
+ }
1611
+
1612
+ // Renders nothing; it exists only to subscribe to the session context and mirror
1613
+ // it onto the bridge. It must live INSIDE SessionProvider for useSession() to
1614
+ // resolve. Mirroring happens in an effect (never during render, which React may
1615
+ // discard or replay) — effects flush on the commit that follows the provider's
1616
+ // fetch, long before any user click.
1617
+ function SessionSnapshotBridge() {
1618
+ const sessionContext = useSession()
1619
+ const status = sessionContext ? sessionContext.status : 'loading'
1620
+ const data = sessionContext ? sessionContext.data : null
1621
+ const update = sessionContext ? sessionContext.update : null
1622
+
1623
+ React.useEffect(
1624
+ function () {
1625
+ teleportNextAuth.status = status
1626
+ teleportNextAuth.session = data || null
1627
+ if (typeof update === 'function') {
1628
+ teleportNextAuth.refreshSession = update
1629
+ }
1630
+ },
1631
+ [status, data, update]
1632
+ )
1633
+
1634
+ return null
1206
1635
  }
1207
1636
 
1208
1637
  export default function AuthSessionProvider(props) {
1209
1638
  return React.createElement(
1210
1639
  SessionProvider,
1211
- { session: props.pageProps && props.pageProps.session ? props.pageProps.session : undefined },
1640
+ {
1641
+ session: props.pageProps && props.pageProps.session ? props.pageProps.session : undefined,
1642
+ // Every regained window focus re-fetched /api/auth/session, and each of
1643
+ // those is a server round trip for a session the page already has. Tabbing
1644
+ // away and back was enough to trigger one. Cross-tab sign-in/sign-out
1645
+ // still propagates: next-auth broadcasts those over its storage channel,
1646
+ // which is independent of this flag.
1647
+ refetchOnWindowFocus: false,
1648
+ },
1649
+ React.createElement(SessionSnapshotBridge, { key: 'teleport-session-bridge' }),
1212
1650
  props.children
1213
1651
  )
1214
1652
  }