@dyrected/core 2.5.16 → 2.5.18

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/dist/index.cjs CHANGED
@@ -1170,19 +1170,27 @@ function parseMongoWhere(where) {
1170
1170
  }
1171
1171
 
1172
1172
  // src/utils/hooks.ts
1173
- async function runCollectionHooks(hooks, args) {
1173
+ async function runCollectionHooks(hooks, args, options = {}) {
1174
1174
  if (!hooks || hooks.length === 0) {
1175
1175
  return args.data ?? args.doc ?? void 0;
1176
1176
  }
1177
1177
  let currentPayload = args.data ?? args.doc ?? void 0;
1178
1178
  for (const hook of hooks) {
1179
- const result = await hook({
1180
- ...args,
1181
- data: args.data !== void 0 ? currentPayload : void 0,
1182
- doc: args.doc !== void 0 ? currentPayload : void 0
1183
- });
1184
- if (result !== void 0) {
1185
- currentPayload = result;
1179
+ try {
1180
+ const result = await hook({
1181
+ ...args,
1182
+ data: args.data !== void 0 ? currentPayload : void 0,
1183
+ doc: args.doc !== void 0 ? currentPayload : void 0
1184
+ });
1185
+ if (result !== void 0) {
1186
+ currentPayload = result;
1187
+ }
1188
+ } catch (err) {
1189
+ if (options.isolated) {
1190
+ console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
1191
+ } else {
1192
+ throw err;
1193
+ }
1186
1194
  }
1187
1195
  }
1188
1196
  return currentPayload;
@@ -1683,7 +1691,7 @@ var CollectionController = class {
1683
1691
  user,
1684
1692
  req: c.req,
1685
1693
  operation: "create"
1686
- });
1694
+ }, { isolated: true });
1687
1695
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1688
1696
  doc,
1689
1697
  req: c.req,
@@ -1741,7 +1749,7 @@ var CollectionController = class {
1741
1749
  user,
1742
1750
  req: c.req,
1743
1751
  operation: "create"
1744
- });
1752
+ }, { isolated: true });
1745
1753
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1746
1754
  doc,
1747
1755
  req: c.req,
@@ -1799,7 +1807,7 @@ var CollectionController = class {
1799
1807
  user,
1800
1808
  req: c.req,
1801
1809
  operation: "update"
1802
- });
1810
+ }, { isolated: true });
1803
1811
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1804
1812
  doc,
1805
1813
  req: c.req,
@@ -1914,7 +1922,7 @@ var CollectionController = class {
1914
1922
  doc,
1915
1923
  user,
1916
1924
  req: c.req
1917
- });
1925
+ }, { isolated: true });
1918
1926
  return c.json({ message: "Deleted" });
1919
1927
  }
1920
1928
  async deleteMany(c) {
@@ -1971,7 +1979,7 @@ var CollectionController = class {
1971
1979
  doc,
1972
1980
  user,
1973
1981
  req: c.req
1974
- });
1982
+ }, { isolated: true });
1975
1983
  } catch (err) {
1976
1984
  failed.push({ id, error: err?.message ?? "Unknown error" });
1977
1985
  }
@@ -2073,7 +2081,7 @@ var GlobalController = class {
2073
2081
  user,
2074
2082
  req: c.req,
2075
2083
  operation: "update"
2076
- });
2084
+ }, { isolated: true });
2077
2085
  const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
2078
2086
  doc: updated,
2079
2087
  req: c.req,
@@ -2305,11 +2313,37 @@ function buildWelcomeEmail(config, args) {
2305
2313
  return {
2306
2314
  subject: custom?.subject ?? "Welcome \u2014 your account is ready",
2307
2315
  html: custom?.html ?? `
2308
- <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
2309
- <h2>Welcome!</h2>
2310
- <p>Your account has been created. You can now log in with:</p>
2311
- <p><strong>${args.email}</strong></p>
2312
- </div>`
2316
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
2317
+ <tr>
2318
+ <td align="center" style="padding:40px 16px">
2319
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
2320
+ <tr>
2321
+ <td style="padding:32px 32px 0">
2322
+ <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
2323
+ <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Welcome!</h1>
2324
+ </td>
2325
+ </tr>
2326
+ <tr>
2327
+ <td style="padding:0 32px">
2328
+ <p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Your account has been created. You can now log in with:</p>
2329
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
2330
+ <tr>
2331
+ <td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
2332
+ ${args.email}
2333
+ </td>
2334
+ </tr>
2335
+ </table>
2336
+ </td>
2337
+ </tr>
2338
+ <tr>
2339
+ <td style="padding:32px">
2340
+ <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't create this account, you can safely ignore this email.</p>
2341
+ </td>
2342
+ </tr>
2343
+ </table>
2344
+ </td>
2345
+ </tr>
2346
+ </table>`
2313
2347
  };
2314
2348
  }
2315
2349
  function buildInviteEmail(config, args) {
@@ -2317,24 +2351,89 @@ function buildInviteEmail(config, args) {
2317
2351
  return {
2318
2352
  subject: custom?.subject ?? "You've been invited",
2319
2353
  html: custom?.html ?? `
2320
- <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
2321
- <h2>You've been invited</h2>
2322
- ${args.invitedByEmail ? `<p>Invited by <strong>${args.invitedByEmail}</strong>.</p>` : ""}
2323
- <p>Use the token below to accept your invitation. It expires in 7 days.</p>
2324
- <pre style="background:#f4f4f4;padding:12px;border-radius:4px;word-break:break-all">${args.token}</pre>
2325
- </div>`
2354
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
2355
+ <tr>
2356
+ <td align="center" style="padding:40px 16px">
2357
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
2358
+ <tr>
2359
+ <td style="padding:32px 32px 0">
2360
+ <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
2361
+ <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">You've been invited</h1>
2362
+ </td>
2363
+ </tr>
2364
+ <tr>
2365
+ <td style="padding:0 32px">
2366
+ ${args.invitedByEmail ? `<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">You were invited by <strong style="color:#111827">${args.invitedByEmail}</strong>.</p>` : ""}
2367
+ <p style="margin:0 0 16px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Use the token below to accept your invitation. It expires in 7 days.</p>
2368
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
2369
+ <tr>
2370
+ <td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
2371
+ ${args.token}
2372
+ </td>
2373
+ </tr>
2374
+ </table>
2375
+ </td>
2376
+ </tr>
2377
+ <tr>
2378
+ <td style="padding:32px">
2379
+ <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you weren't expecting this invitation, you can safely ignore this email.</p>
2380
+ </td>
2381
+ </tr>
2382
+ </table>
2383
+ </td>
2384
+ </tr>
2385
+ </table>`
2326
2386
  };
2327
2387
  }
2328
2388
  function buildResetPasswordEmail(config, args) {
2329
2389
  const custom = config.email?.templates?.resetPassword?.(args);
2390
+ const resetLink = args.url;
2330
2391
  return {
2331
2392
  subject: custom?.subject ?? "Reset your password",
2332
2393
  html: custom?.html ?? `
2333
- <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
2334
- <h2>Reset your password</h2>
2335
- <p>Use the token below to reset your password. It expires in 1 hour.</p>
2336
- <pre style="background:#f4f4f4;padding:12px;border-radius:4px;word-break:break-all">${args.token}</pre>
2337
- </div>`
2394
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
2395
+ <tr>
2396
+ <td align="center" style="padding:40px 16px">
2397
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
2398
+ <tr>
2399
+ <td style="padding:32px 32px 0">
2400
+ <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
2401
+ <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Reset your password</h1>
2402
+ </td>
2403
+ </tr>
2404
+ <tr>
2405
+ <td style="padding:0 32px">
2406
+ <p style="margin:0 0 24px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">We received a request to reset your password. Use the button below to set a new password. It will expire in 1 hour.</p>
2407
+ ${resetLink ? `
2408
+ <table cellpadding="0" cellspacing="0" border="0" style="margin-bottom:24px">
2409
+ <tr>
2410
+ <td style="border-radius:6px;background-color:#111827">
2411
+ <a href="${resetLink}" style="display:inline-block;padding:12px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;font-family:sans-serif;border-radius:6px">
2412
+ Reset Password
2413
+ </a>
2414
+ </td>
2415
+ </tr>
2416
+ </table>
2417
+ ` : ""}
2418
+ <p style="margin:0 0 8px;font-size:12px;color:#9ca3af;font-family:sans-serif">Or copy and paste this token manually in the admin dashboard:</p>
2419
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
2420
+ <tr>
2421
+ <td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
2422
+ ${args.token}
2423
+ </td>
2424
+ </tr>
2425
+ </table>
2426
+ </td>
2427
+ </tr>
2428
+ <tr>
2429
+ <td style="padding:32px">
2430
+ <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't request a password reset, you can safely ignore this email.</p>
2431
+ </td>
2432
+ </tr>
2433
+ </table>
2434
+ </td>
2435
+ </tr>
2436
+ </table>`
2338
2437
  };
2339
2438
  }
2340
2439
  function buildPasswordChangedEmail(config, args) {
@@ -2342,11 +2441,44 @@ function buildPasswordChangedEmail(config, args) {
2342
2441
  return {
2343
2442
  subject: custom?.subject ?? "Your password has been changed",
2344
2443
  html: custom?.html ?? `
2345
- <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
2346
- <h2>Password changed</h2>
2347
- <p>The password for <strong>${args.email}</strong> was just changed.</p>
2348
- <p>If you did not make this change, please contact support immediately.</p>
2349
- </div>`
2444
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
2445
+ <tr>
2446
+ <td align="center" style="padding:40px 16px">
2447
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
2448
+ <tr>
2449
+ <td style="padding:32px 32px 0">
2450
+ <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
2451
+ <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Password changed</h1>
2452
+ </td>
2453
+ </tr>
2454
+ <tr>
2455
+ <td style="padding:0 32px">
2456
+ <p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">The password for your account was just changed:</p>
2457
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
2458
+ <tr>
2459
+ <td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
2460
+ ${args.email}
2461
+ </td>
2462
+ </tr>
2463
+ </table>
2464
+ <table cellpadding="0" cellspacing="0" border="0" style="width:100%;margin-top:16px;background-color:#fef2f2;border-radius:6px;border:1px solid #fecaca;table-layout:fixed">
2465
+ <tr>
2466
+ <td style="padding:12px 16px;font-size:13px;color:#b91c1c;line-height:1.5;font-family:sans-serif">
2467
+ If you did not make this change, please contact support immediately.
2468
+ </td>
2469
+ </tr>
2470
+ </table>
2471
+ </td>
2472
+ </tr>
2473
+ <tr>
2474
+ <td style="padding:32px">
2475
+ <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">This is an automated security notification.</p>
2476
+ </td>
2477
+ </tr>
2478
+ </table>
2479
+ </td>
2480
+ </tr>
2481
+ </table>`
2350
2482
  };
2351
2483
  }
2352
2484
 
@@ -2507,8 +2639,10 @@ var AuthController = class {
2507
2639
  { sub: user.id, email: user.email, collection: this.collection.slug, purpose: "reset" },
2508
2640
  "1h"
2509
2641
  );
2642
+ const resetUrl = body?.resetUrl;
2643
+ const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
2510
2644
  try {
2511
- const { subject, html } = buildResetPasswordEmail(config, { token: resetToken });
2645
+ const { subject, html } = buildResetPasswordEmail(config, { token: resetToken, url });
2512
2646
  await sendEmail(config, { to: user.email, subject, html });
2513
2647
  } catch (err) {
2514
2648
  console.error("[dyrected/core] Failed to send password reset email:", err);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DyrectedConfig, F as Field, C as CollectionConfig, P as Prettify, I as InferDocShape, S as SystemDocFields, A as AuthDocFields, U as UploadDocFields, G as GlobalConfig } from './app-DO1s9YW1.cjs';
2
- export { a as AccessFunction, b as AdminConfig, c as AuthenticatedUser, B as BaseDocument, d as Block, e as CollectionAfterChangeHook, f as CollectionAfterDeleteHook, g as CollectionAfterReadHook, h as CollectionBeforeChangeHook, i as CollectionBeforeDeleteHook, j as CollectionBeforeReadHook, k as DatabaseAdapter, l as DynamicOptionItem, m as DynamicOptionsConfig, n as DynamicOptionsResolver, o as DynamicOptionsResolverArgs, p as DyrectedContext, q as FieldAfterReadHook, r as FieldBeforeChangeHook, s as FieldHook, t as FieldType, u as FileData, v as GlobalAfterChangeHook, w as GlobalAfterReadHook, x as GlobalBeforeChangeHook, y as GlobalBeforeReadHook, H as HookFunction, z as HookRequestContext, E as ImageService, J as PaginatedResult, K as StorageAdapter, L as UploadConfig, M as createDyrectedApp } from './app-DO1s9YW1.cjs';
1
+ import { D as DyrectedConfig, F as Field, C as CollectionConfig, P as Prettify, I as InferDocShape, S as SystemDocFields, A as AuthDocFields, U as UploadDocFields, G as GlobalConfig } from './app-D0ffogDd.cjs';
2
+ export { a as AccessFunction, b as AdminConfig, c as AuthenticatedUser, B as BaseDocument, d as Block, e as CollectionAfterChangeHook, f as CollectionAfterDeleteHook, g as CollectionAfterReadHook, h as CollectionBeforeChangeHook, i as CollectionBeforeDeleteHook, j as CollectionBeforeReadHook, k as DatabaseAdapter, l as DynamicOptionItem, m as DynamicOptionsConfig, n as DynamicOptionsResolver, o as DynamicOptionsResolverArgs, p as DyrectedContext, q as FieldAfterReadHook, r as FieldBeforeChangeHook, s as FieldHook, t as FieldType, u as FileData, v as GlobalAfterChangeHook, w as GlobalAfterReadHook, x as GlobalBeforeChangeHook, y as GlobalBeforeReadHook, H as HookFunction, z as HookRequestContext, E as ImageService, J as PaginatedResult, K as StorageAdapter, L as UploadConfig, M as createDyrectedApp } from './app-D0ffogDd.cjs';
3
3
  import 'hono/types';
4
4
  import 'hono';
5
5
 
@@ -91,6 +91,11 @@ type AnyHookFn = (args: any) => any;
91
91
  * Each hook receives the output of the previous hook merged back into the
92
92
  * original `args`. When a hook returns a non-undefined value it becomes the
93
93
  * `data` / `doc` for the next hook in the chain.
94
+ *
95
+ * Pass `{ isolated: true }` for hooks that run **after** a DB write has already
96
+ * been committed (`afterChange`, `afterDelete`). In isolated mode each hook is
97
+ * wrapped in a try/catch so a failing side-effect (email, webhook, etc.) never
98
+ * surfaces as an HTTP 500 to the caller — the write already succeeded.
94
99
  */
95
100
  declare function runCollectionHooks(hooks: AnyHookFn[] | undefined, args: {
96
101
  data?: unknown;
@@ -99,6 +104,8 @@ declare function runCollectionHooks(hooks: AnyHookFn[] | undefined, args: {
99
104
  req?: unknown;
100
105
  operation?: "create" | "update" | "delete";
101
106
  [key: string]: unknown;
107
+ }, options?: {
108
+ isolated?: boolean;
102
109
  }): Promise<any>;
103
110
  /**
104
111
  * Execute field-level `beforeChange` hooks recursively on a data payload.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DyrectedConfig, F as Field, C as CollectionConfig, P as Prettify, I as InferDocShape, S as SystemDocFields, A as AuthDocFields, U as UploadDocFields, G as GlobalConfig } from './app-DO1s9YW1.js';
2
- export { a as AccessFunction, b as AdminConfig, c as AuthenticatedUser, B as BaseDocument, d as Block, e as CollectionAfterChangeHook, f as CollectionAfterDeleteHook, g as CollectionAfterReadHook, h as CollectionBeforeChangeHook, i as CollectionBeforeDeleteHook, j as CollectionBeforeReadHook, k as DatabaseAdapter, l as DynamicOptionItem, m as DynamicOptionsConfig, n as DynamicOptionsResolver, o as DynamicOptionsResolverArgs, p as DyrectedContext, q as FieldAfterReadHook, r as FieldBeforeChangeHook, s as FieldHook, t as FieldType, u as FileData, v as GlobalAfterChangeHook, w as GlobalAfterReadHook, x as GlobalBeforeChangeHook, y as GlobalBeforeReadHook, H as HookFunction, z as HookRequestContext, E as ImageService, J as PaginatedResult, K as StorageAdapter, L as UploadConfig, M as createDyrectedApp } from './app-DO1s9YW1.js';
1
+ import { D as DyrectedConfig, F as Field, C as CollectionConfig, P as Prettify, I as InferDocShape, S as SystemDocFields, A as AuthDocFields, U as UploadDocFields, G as GlobalConfig } from './app-D0ffogDd.js';
2
+ export { a as AccessFunction, b as AdminConfig, c as AuthenticatedUser, B as BaseDocument, d as Block, e as CollectionAfterChangeHook, f as CollectionAfterDeleteHook, g as CollectionAfterReadHook, h as CollectionBeforeChangeHook, i as CollectionBeforeDeleteHook, j as CollectionBeforeReadHook, k as DatabaseAdapter, l as DynamicOptionItem, m as DynamicOptionsConfig, n as DynamicOptionsResolver, o as DynamicOptionsResolverArgs, p as DyrectedContext, q as FieldAfterReadHook, r as FieldBeforeChangeHook, s as FieldHook, t as FieldType, u as FileData, v as GlobalAfterChangeHook, w as GlobalAfterReadHook, x as GlobalBeforeChangeHook, y as GlobalBeforeReadHook, H as HookFunction, z as HookRequestContext, E as ImageService, J as PaginatedResult, K as StorageAdapter, L as UploadConfig, M as createDyrectedApp } from './app-D0ffogDd.js';
3
3
  import 'hono/types';
4
4
  import 'hono';
5
5
 
@@ -91,6 +91,11 @@ type AnyHookFn = (args: any) => any;
91
91
  * Each hook receives the output of the previous hook merged back into the
92
92
  * original `args`. When a hook returns a non-undefined value it becomes the
93
93
  * `data` / `doc` for the next hook in the chain.
94
+ *
95
+ * Pass `{ isolated: true }` for hooks that run **after** a DB write has already
96
+ * been committed (`afterChange`, `afterDelete`). In isolated mode each hook is
97
+ * wrapped in a try/catch so a failing side-effect (email, webhook, etc.) never
98
+ * surfaces as an HTTP 500 to the caller — the write already succeeded.
94
99
  */
95
100
  declare function runCollectionHooks(hooks: AnyHookFn[] | undefined, args: {
96
101
  data?: unknown;
@@ -99,6 +104,8 @@ declare function runCollectionHooks(hooks: AnyHookFn[] | undefined, args: {
99
104
  req?: unknown;
100
105
  operation?: "create" | "update" | "delete";
101
106
  [key: string]: unknown;
107
+ }, options?: {
108
+ isolated?: boolean;
102
109
  }): Promise<any>;
103
110
  /**
104
111
  * Execute field-level `beforeChange` hooks recursively on a data payload.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  executeFieldBeforeChange,
5
5
  normalizeConfig,
6
6
  runCollectionHooks
7
- } from "./chunk-2JMA3M5S.js";
7
+ } from "./chunk-TUUHGLB5.js";
8
8
 
9
9
  // src/utils/setup-prompt.ts
10
10
  function buildEnvironmentSection(frameworkLabel, isSelfHosted, config) {