@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.
- package/__tests__/auth-env-secret-preservation.test.ts +35 -0
- package/__tests__/auth-nextauth-url.test.ts +28 -0
- package/__tests__/auth-session-cost.test.ts +614 -0
- package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
- package/__tests__/data-node-not-awaited.test.ts +644 -0
- package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
- package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
- package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
- package/__tests__/raw-sql-param-binding.test.ts +82 -0
- package/__tests__/reserved-word-state-binding.test.ts +112 -0
- package/__tests__/runtime-stringified-boolean-truthiness.test.ts +76 -0
- package/__tests__/strip-event-handler-attrs.test.ts +59 -0
- package/__tests__/workflow-auth-guard.test.ts +277 -0
- package/dist/cjs/api-route-generator.d.ts +3 -3
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +136 -11
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +67 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +458 -20
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +31 -0
- package/dist/cjs/await-result.d.ts.map +1 -0
- package/dist/cjs/await-result.js +50 -0
- package/dist/cjs/await-result.js.map +1 -0
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +162 -3
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.js +44 -11
- package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-login.js +10 -1
- package/dist/cjs/nodes/account/account-login.js.map +1 -1
- package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/cjs/raw-sql-param-binding.js +32 -0
- package/dist/cjs/raw-sql-param-binding.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +15 -7
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-auth-generator.d.ts +26 -0
- package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
- package/dist/cjs/workflow-auth-generator.js +166 -0
- package/dist/cjs/workflow-auth-generator.js.map +1 -0
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +107 -16
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts +2 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +170 -20
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts +3 -3
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +136 -11
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +67 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +453 -19
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +31 -0
- package/dist/esm/await-result.d.ts.map +1 -0
- package/dist/esm/await-result.js +45 -0
- package/dist/esm/await-result.js.map +1 -0
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +162 -3
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-get-current.js +44 -11
- package/dist/esm/nodes/account/account-get-current.js.map +1 -1
- package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-login.js +10 -1
- package/dist/esm/nodes/account/account-login.js.map +1 -1
- package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/esm/raw-sql-param-binding.js +32 -0
- package/dist/esm/raw-sql-param-binding.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +15 -7
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-auth-generator.d.ts +26 -0
- package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
- package/dist/esm/workflow-auth-generator.js +161 -0
- package/dist/esm/workflow-auth-generator.js.map +1 -0
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +108 -17
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts +2 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +169 -20
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +139 -11
- package/src/auth-generator.ts +471 -19
- package/src/await-result.ts +52 -0
- package/src/executor-generator.ts +162 -3
- package/src/nodes/account/account-get-current.ts +44 -9
- package/src/nodes/account/account-login.ts +12 -1
- package/src/raw-sql-param-binding.ts +33 -0
- package/src/segment-splitter.ts +15 -7
- package/src/workflow-auth-generator.ts +187 -0
- package/src/workflow-component-plugin.ts +129 -20
- 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
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
safe[
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
|
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
|
-
|
|
1191
|
-
import
|
|
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 =
|
|
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
|
-
{
|
|
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
|
}
|