@teleporthq/teleport-plugin-next-workflows 0.43.45 → 0.43.47

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 (112) hide show
  1. package/__tests__/_helpers/run-generated-middleware.ts +130 -0
  2. package/__tests__/auth-env-secret-preservation.test.ts +35 -0
  3. package/__tests__/auth-nextauth-url.test.ts +28 -0
  4. package/__tests__/auth-session-cost.test.ts +614 -0
  5. package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
  6. package/__tests__/data-node-not-awaited.test.ts +644 -0
  7. package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
  8. package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
  9. package/__tests__/form-submitted-trigger-context.test.ts +100 -0
  10. package/__tests__/middleware-guest-order-details.test.ts +258 -0
  11. package/__tests__/middleware-row-owned-pages.test.ts +32 -9
  12. package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
  13. package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
  14. package/__tests__/raw-sql-param-binding.test.ts +82 -0
  15. package/__tests__/reserved-word-state-binding.test.ts +112 -0
  16. package/__tests__/strip-event-handler-attrs.test.ts +59 -0
  17. package/__tests__/workflow-auth-guard.test.ts +277 -0
  18. package/dist/cjs/api-route-generator.d.ts +3 -3
  19. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  20. package/dist/cjs/api-route-generator.js +136 -11
  21. package/dist/cjs/api-route-generator.js.map +1 -1
  22. package/dist/cjs/auth-generator.d.ts +67 -1
  23. package/dist/cjs/auth-generator.d.ts.map +1 -1
  24. package/dist/cjs/auth-generator.js +573 -31
  25. package/dist/cjs/auth-generator.js.map +1 -1
  26. package/dist/cjs/await-result.d.ts +31 -0
  27. package/dist/cjs/await-result.d.ts.map +1 -0
  28. package/dist/cjs/await-result.js +50 -0
  29. package/dist/cjs/await-result.js.map +1 -0
  30. package/dist/cjs/executor-generator.d.ts.map +1 -1
  31. package/dist/cjs/executor-generator.js +150 -1
  32. package/dist/cjs/executor-generator.js.map +1 -1
  33. package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
  34. package/dist/cjs/nodes/account/account-get-current.js +44 -11
  35. package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
  36. package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
  37. package/dist/cjs/nodes/account/account-login.js +10 -1
  38. package/dist/cjs/nodes/account/account-login.js.map +1 -1
  39. package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
  40. package/dist/cjs/raw-sql-param-binding.js +32 -0
  41. package/dist/cjs/raw-sql-param-binding.js.map +1 -1
  42. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  43. package/dist/cjs/segment-splitter.js +15 -7
  44. package/dist/cjs/segment-splitter.js.map +1 -1
  45. package/dist/cjs/trigger-generator.js +7 -1
  46. package/dist/cjs/trigger-generator.js.map +1 -1
  47. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  48. package/dist/cjs/workflow-auth-generator.d.ts +26 -0
  49. package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
  50. package/dist/cjs/workflow-auth-generator.js +166 -0
  51. package/dist/cjs/workflow-auth-generator.js.map +1 -0
  52. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  53. package/dist/cjs/workflow-component-plugin.js +124 -17
  54. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  55. package/dist/cjs/workflow-project-plugin.d.ts +2 -1
  56. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  57. package/dist/cjs/workflow-project-plugin.js +170 -20
  58. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  59. package/dist/esm/api-route-generator.d.ts +3 -3
  60. package/dist/esm/api-route-generator.d.ts.map +1 -1
  61. package/dist/esm/api-route-generator.js +136 -11
  62. package/dist/esm/api-route-generator.js.map +1 -1
  63. package/dist/esm/auth-generator.d.ts +67 -1
  64. package/dist/esm/auth-generator.d.ts.map +1 -1
  65. package/dist/esm/auth-generator.js +568 -30
  66. package/dist/esm/auth-generator.js.map +1 -1
  67. package/dist/esm/await-result.d.ts +31 -0
  68. package/dist/esm/await-result.d.ts.map +1 -0
  69. package/dist/esm/await-result.js +45 -0
  70. package/dist/esm/await-result.js.map +1 -0
  71. package/dist/esm/executor-generator.d.ts.map +1 -1
  72. package/dist/esm/executor-generator.js +150 -1
  73. package/dist/esm/executor-generator.js.map +1 -1
  74. package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
  75. package/dist/esm/nodes/account/account-get-current.js +44 -11
  76. package/dist/esm/nodes/account/account-get-current.js.map +1 -1
  77. package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
  78. package/dist/esm/nodes/account/account-login.js +10 -1
  79. package/dist/esm/nodes/account/account-login.js.map +1 -1
  80. package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
  81. package/dist/esm/raw-sql-param-binding.js +32 -0
  82. package/dist/esm/raw-sql-param-binding.js.map +1 -1
  83. package/dist/esm/segment-splitter.d.ts.map +1 -1
  84. package/dist/esm/segment-splitter.js +15 -7
  85. package/dist/esm/segment-splitter.js.map +1 -1
  86. package/dist/esm/trigger-generator.js +7 -1
  87. package/dist/esm/trigger-generator.js.map +1 -1
  88. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  89. package/dist/esm/workflow-auth-generator.d.ts +26 -0
  90. package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
  91. package/dist/esm/workflow-auth-generator.js +161 -0
  92. package/dist/esm/workflow-auth-generator.js.map +1 -0
  93. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  94. package/dist/esm/workflow-component-plugin.js +125 -18
  95. package/dist/esm/workflow-component-plugin.js.map +1 -1
  96. package/dist/esm/workflow-project-plugin.d.ts +2 -1
  97. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  98. package/dist/esm/workflow-project-plugin.js +169 -20
  99. package/dist/esm/workflow-project-plugin.js.map +1 -1
  100. package/package.json +4 -4
  101. package/src/api-route-generator.ts +139 -11
  102. package/src/auth-generator.ts +600 -30
  103. package/src/await-result.ts +52 -0
  104. package/src/executor-generator.ts +150 -1
  105. package/src/nodes/account/account-get-current.ts +44 -9
  106. package/src/nodes/account/account-login.ts +12 -1
  107. package/src/raw-sql-param-binding.ts +33 -0
  108. package/src/segment-splitter.ts +15 -7
  109. package/src/trigger-generator.ts +7 -1
  110. package/src/workflow-auth-generator.ts +187 -0
  111. package/src/workflow-component-plugin.ts +146 -21
  112. 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');
599
+ }
600
+ const { verifyPassword } = require('./hash-password');
601
+ const { classifyAuthInfrastructureError } = require('./db-health');
602
+ let user;
603
+ try {
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');
495
609
  }
496
610
  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;
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 */
@@ -835,17 +1148,50 @@ const isRowOwnedSelfGuardedPage = (protection) => {
835
1148
  const roles = protection.allowedRoles || [];
836
1149
  return roles.length === 0;
837
1150
  };
1151
+ // The exact route pattern a self-guarded page serves, e.g.
1152
+ // `/orders/[order_number]`. Skipping the page from `protectedRoutes` is not
1153
+ // enough on its own: platform list/details pairs deliberately share a static
1154
+ // base (`/orders` listing + `/orders/[order_number]` details) so the protected
1155
+ // LISTING route prefix-matches — and bounces — every details request. Knowing
1156
+ // the details pattern lets the middleware waive that inherited protection for
1157
+ // exactly the paths the self-guarded page owns.
1158
+ //
1159
+ // `routePattern` is the authoritative value; UIDLs emitted before that field
1160
+ // existed are reconstructed from `route` + `rowOwnerDifferentiator`.
1161
+ const selfGuardedRoutePatternOf = (protection) => {
1162
+ if (protection.routePattern) {
1163
+ return protection.routePattern;
1164
+ }
1165
+ const base = protection.route || '';
1166
+ const differentiator = protection.rowOwnerDifferentiator;
1167
+ if (!base || !differentiator) {
1168
+ return base;
1169
+ }
1170
+ return base === '/' ? `/[${differentiator}]` : `${base}/[${differentiator}]`;
1171
+ };
838
1172
  const generateMiddlewareFile = (auth) => {
839
1173
  var _a, _b, _c, _d;
840
1174
  const protectedRoutes = {};
1175
+ // Page ids skipped from `protectedRoutes` because their page-load SQL is the
1176
+ // access control. Tracked by id (not by route) so the folder pass below can
1177
+ // revoke the waiver for a page that a role-gated folder pulls back under
1178
+ // middleware protection.
1179
+ const selfGuardedPageIds = new Set();
841
1180
  if (auth.pageProtection) {
842
- for (const protection of Object.values(auth.pageProtection)) {
1181
+ for (const [pageId, protection] of Object.entries(auth.pageProtection)) {
843
1182
  if (isRowOwnedSelfGuardedPage(protection)) {
1183
+ selfGuardedPageIds.add(pageId);
844
1184
  continue;
845
1185
  }
1186
+ // A route key can be claimed by more than one page — a list/details pair
1187
+ // deliberately shares its static base (`/orders` + `/orders/[id]`), and a
1188
+ // user can point two pages at the same custom URL. One prefix key gates
1189
+ // the whole subtree, so merge instead of letting document order decide
1190
+ // which page's rules survive.
1191
+ const existing = protectedRoutes[protection.route];
846
1192
  protectedRoutes[protection.route] = {
847
- requiresAuth: protection.requiresAuth,
848
- allowedRoles: protection.allowedRoles || [],
1193
+ requiresAuth: !!(existing === null || existing === void 0 ? void 0 : existing.requiresAuth) || protection.requiresAuth,
1194
+ allowedRoles: Array.from(new Set([...((existing === null || existing === void 0 ? void 0 : existing.allowedRoles) || []), ...(protection.allowedRoles || [])])),
849
1195
  };
850
1196
  }
851
1197
  }
@@ -864,6 +1210,10 @@ const generateMiddlewareFile = (auth) => {
864
1210
  if (isRowOwnedSelfGuardedPage(pageProt) && folderRoles.length === 0) {
865
1211
  continue;
866
1212
  }
1213
+ // The folder imposes a role the row-level SQL cannot reproduce,
1214
+ // so this page is back under middleware protection and must not
1215
+ // keep its self-guarded waiver.
1216
+ selfGuardedPageIds.delete(childId);
867
1217
  const existingRoles = ((_b = protectedRoutes[pageProt.route]) === null || _b === void 0 ? void 0 : _b.allowedRoles) || [];
868
1218
  const mergedRoles = Array.from(new Set([...existingRoles, ...folderRoles]));
869
1219
  protectedRoutes[pageProt.route] = {
@@ -876,6 +1226,9 @@ const generateMiddlewareFile = (auth) => {
876
1226
  }
877
1227
  }
878
1228
  }
1229
+ const selfGuardedRoutes = Array.from(new Set(Array.from(selfGuardedPageIds)
1230
+ .map((pageId) => selfGuardedRoutePatternOf(auth.pageProtection[pageId]))
1231
+ .filter((pattern) => !!pattern)));
879
1232
  const authRoutes = [];
880
1233
  if (auth.authPages.signIn) {
881
1234
  authRoutes.push(auth.authPages.signIn.route);
@@ -903,6 +1256,7 @@ const generateMiddlewareFile = (auth) => {
903
1256
  }
904
1257
  const protectedRoutesJson = JSON.stringify(protectedRoutes, null, 2);
905
1258
  const authRoutesJson = JSON.stringify(authRoutes);
1259
+ const selfGuardedRoutesJson = JSON.stringify(selfGuardedRoutes);
906
1260
  const signInRoute = ((_d = auth.authPages.signIn) === null || _d === void 0 ? void 0 : _d.route) || '/auth/sign-in';
907
1261
  return `import { NextResponse } from 'next/server';
908
1262
  import { getToken } from 'next-auth/jwt';
@@ -911,6 +1265,15 @@ const protectedRoutes = ${protectedRoutesJson};
911
1265
 
912
1266
  const authRoutes = ${authRoutesJson};
913
1267
 
1268
+ // Routes whose page-load SQL is the access control: the query filters rows by
1269
+ // the visitor's user id OR the persistent anonymous-localStorage UUID, so a
1270
+ // guest buyer must be allowed to reach the row they own. These pages are
1271
+ // already absent from protectedRoutes, but a platform list/details pair shares
1272
+ // its static base ("/orders" listing + "/orders/[order_number]" details), so
1273
+ // without this list the protected LISTING route prefix-matches every details
1274
+ // request and redirects the guest to sign-in.
1275
+ const selfGuardedRoutes = ${selfGuardedRoutesJson};
1276
+
914
1277
  function getUserRoleFromToken(token) {
915
1278
  if (!token || typeof token !== 'object') return null;
916
1279
  var r = token.role;
@@ -958,6 +1321,31 @@ function hasSessionCookie(request) {
958
1321
  return false;
959
1322
  }
960
1323
 
1324
+ // Turns a Next.js route pattern into an anchored matcher: "[param]" matches a
1325
+ // single path segment, "[...param]" / "[[...param]]" match one or more. Every
1326
+ // other character is escaped so a literal "." or "+" in a slug can never widen
1327
+ // the match. Brackets are deliberately NOT escaped in the first pass — they are
1328
+ // the placeholder syntax the second pass rewrites.
1329
+ function compileRoutePattern(pattern) {
1330
+ var escaped = String(pattern).replace(/[.*+?^\${}()|\\\\]/g, '\\\\$&');
1331
+ var source = escaped
1332
+ .replace(/\\[\\[\\.\\.\\.[^\\]]*\\]\\]/g, '.+')
1333
+ .replace(/\\[\\.\\.\\.[^\\]]*\\]/g, '.+')
1334
+ .replace(/\\[[^\\]]*\\]/g, '[^/]+');
1335
+ return new RegExp('^' + source + '$');
1336
+ }
1337
+
1338
+ const selfGuardedRouteMatchers = selfGuardedRoutes.map(compileRoutePattern);
1339
+
1340
+ function isSelfGuardedPath(pathname) {
1341
+ for (var i = 0; i < selfGuardedRouteMatchers.length; i++) {
1342
+ if (selfGuardedRouteMatchers[i].test(pathname)) {
1343
+ return true;
1344
+ }
1345
+ }
1346
+ return false;
1347
+ }
1348
+
961
1349
  // Where to send an authenticated user who lacks the required role. Normally the
962
1350
  // home page ("you don't have access, here's the public site"). But when "/" is
963
1351
  // itself a protected page (e.g. an admin dashboard published at the root), a
@@ -988,14 +1376,29 @@ async function middleware(request) {
988
1376
  return NextResponse.next();
989
1377
  }
990
1378
 
991
- let matchedProtection = null;
992
- const routes = Object.keys(protectedRoutes).sort(function(a, b) {
993
- return b.length - a.length;
994
- });
995
- for (let r = 0; r < routes.length; r++) {
996
- if (pathname === routes[r] || pathname.startsWith(routes[r] + '/')) {
997
- matchedProtection = protectedRoutes[routes[r]];
998
- break;
1379
+ // Route keys carry no trailing slash, so neither may the path we match with.
1380
+ // Next.js serves "/orders/ORD-42/" and "/orders/ORD-42" as the same route;
1381
+ // matching the raw pathname would let the slashed form miss every exact key
1382
+ // (and every self-guarded pattern) and fall back to the ancestor prefix.
1383
+ // The ORIGINAL pathname is what goes into the sign-in callbackUrl.
1384
+ const matchPath =
1385
+ pathname.length > 1 && pathname.endsWith('/') ? pathname.replace(/\\/+$/, '') : pathname;
1386
+
1387
+ // An exact hit is the page's OWN protection and always wins. Anything else is
1388
+ // inherited from an ancestor route (a listing page, an "/admin" subtree, …).
1389
+ const exactProtection = Object.prototype.hasOwnProperty.call(protectedRoutes, matchPath)
1390
+ ? protectedRoutes[matchPath]
1391
+ : null;
1392
+ let matchedProtection = exactProtection;
1393
+ if (!matchedProtection) {
1394
+ const routes = Object.keys(protectedRoutes).sort(function(a, b) {
1395
+ return b.length - a.length;
1396
+ });
1397
+ for (let r = 0; r < routes.length; r++) {
1398
+ if (matchPath.startsWith(routes[r] + '/')) {
1399
+ matchedProtection = protectedRoutes[routes[r]];
1400
+ break;
1401
+ }
999
1402
  }
1000
1403
  }
1001
1404
 
@@ -1003,6 +1406,20 @@ async function middleware(request) {
1003
1406
  return NextResponse.next();
1004
1407
  }
1005
1408
 
1409
+ // Self-guarded details route reached through an ancestor's protection: let it
1410
+ // through so the page-load SQL can decide, using the visitor's user id OR
1411
+ // their anonymous UUID. Deliberately narrow — the waiver never applies to the
1412
+ // page's own exact protection, and never to an ancestor that demands a role,
1413
+ // because role membership is an absolute gate no row-level WHERE clause can
1414
+ // reproduce.
1415
+ if (
1416
+ !exactProtection &&
1417
+ (matchedProtection.allowedRoles || []).length === 0 &&
1418
+ isSelfGuardedPath(matchPath)
1419
+ ) {
1420
+ return NextResponse.next();
1421
+ }
1422
+
1006
1423
  // Resolve the current user for this request. Fast path: decode the session JWT
1007
1424
  // at the edge with getToken. That can return null in the Edge runtime even for
1008
1425
  // a valid session (NEXTAUTH_SECRET not identical/available in the Edge runtime,
@@ -1072,6 +1489,55 @@ export const config = {
1072
1489
  `;
1073
1490
  };
1074
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;
1075
1541
  const generateSessionProviderWrapper = () => {
1076
1542
  // Authored as pure ESM. _app.js imports this with an ESM default import
1077
1543
  // (`import AuthSessionProvider from '.../session-provider'`). When the file
@@ -1083,8 +1549,15 @@ const generateSessionProviderWrapper = () => {
1083
1549
  // it worked locally). Keeping a single, consistent module system removes the
1084
1550
  // fragile boundary. `SessionProvider` is imported by name (the SWC-safe form
1085
1551
  // even though next-auth/react ships CommonJS).
1086
- return `import React from 'react'
1087
- 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'
1088
1561
 
1089
1562
  // Bridge for the workflow account handlers (account-login / -logout / -signup /
1090
1563
  // -social-login). Those handlers are emitted via fn.toString() and re-bundled
@@ -1097,14 +1570,83 @@ import { SessionProvider, signIn, signOut } from 'next-auth/react'
1097
1570
  // next-auth/react that is reliably bundled into the app shell) guarantees the
1098
1571
  // handlers can read them off \`window\` without any fragile require/import of
1099
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
+
1100
1608
  if (typeof window !== 'undefined') {
1101
- 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
1102
1635
  }
1103
1636
 
1104
1637
  export default function AuthSessionProvider(props) {
1105
1638
  return React.createElement(
1106
1639
  SessionProvider,
1107
- { 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' }),
1108
1650
  props.children
1109
1651
  )
1110
1652
  }