@foxford/den 3.7.0 → 4.1.0

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 (40) hide show
  1. package/README.mdx +58 -7
  2. package/adapter.d.cts +1 -1
  3. package/adapter.d.ts +1 -1
  4. package/bin/den.cjs +3 -3
  5. package/bin/den.js +1 -1
  6. package/builder/index.cjs +2 -2
  7. package/builder/index.d.cts +4 -2
  8. package/builder/index.d.ts +4 -2
  9. package/builder/index.js +1 -1
  10. package/{chunk-2GTQK2PZ.js → chunk-7FOG2MJV.js} +9 -6
  11. package/chunk-A2CYL5LY.js +240 -0
  12. package/chunk-HJFBEJT6.cjs +240 -0
  13. package/{chunk-P4J4M2WZ.cjs → chunk-IATA5QZ7.cjs} +9 -6
  14. package/{define-repository-AO7JI6Cb.d.ts → define-repository-Ck45La_2.d.ts} +1 -1
  15. package/{define-repository-C0MI-J32.d.cts → define-repository-DAyeXF48.d.cts} +1 -1
  16. package/{define-slot-CdWdFM16.d.ts → define-slot-BTlLQ__T.d.ts} +1 -1
  17. package/{define-slot-BEeHNEph.d.cts → define-slot-C6hrdJS3.d.cts} +1 -1
  18. package/define.d.cts +3 -3
  19. package/define.d.ts +3 -3
  20. package/{descriptor-BSaHM5B4.d.ts → descriptor-CNJFrEbS.d.ts} +2 -2
  21. package/{descriptor-C2j6Zz6c.d.cts → descriptor-DmfJP4jQ.d.cts} +2 -2
  22. package/index.d.cts +6 -6
  23. package/index.d.ts +6 -6
  24. package/island/index.d.cts +9 -6
  25. package/island/index.d.ts +9 -6
  26. package/ops/index.cjs +17 -0
  27. package/ops/index.d.cts +39 -0
  28. package/ops/index.d.ts +39 -0
  29. package/ops/index.js +17 -0
  30. package/package.json +25 -16
  31. package/{routes-BvV405k4.d.cts → routes-DQ4ud4bu.d.cts} +1 -1
  32. package/{routes-Di3K_0uQ.d.ts → routes-TkIxQ0Uc.d.ts} +1 -1
  33. package/serve/index.cjs +270 -17
  34. package/serve/index.d.cts +21 -6
  35. package/serve/index.d.ts +21 -6
  36. package/serve/index.js +273 -20
  37. package/{types-BxMQ-VOd.d.cts → types-Ccd5bmt1.d.cts} +23 -4
  38. package/{types-BxMQ-VOd.d.ts → types-Ccd5bmt1.d.ts} +23 -4
  39. package/{view-adapter-DMLlp3Iu.d.cts → view-adapter-CrHh7CjB.d.cts} +2 -2
  40. package/{view-adapter-DNLRSFka.d.ts → view-adapter-DPDvv09J.d.ts} +2 -2
package/serve/index.js CHANGED
@@ -13,12 +13,21 @@ import {
13
13
  logger
14
14
  } from "../chunk-TPBI6TOU.js";
15
15
  import {
16
- __async
16
+ closeHttpServer,
17
+ clusterMetrics,
18
+ getProcessMetrics,
19
+ isOpsPath,
20
+ startOpsServer
21
+ } from "../chunk-A2CYL5LY.js";
22
+ import {
23
+ __async,
24
+ __spreadProps,
25
+ __spreadValues
17
26
  } from "../chunk-6FNC3XMI.js";
18
27
 
19
28
  // src/serve/index.ts
20
29
  import Fastify from "fastify";
21
- import { log as log2 } from "@foxford/logger";
30
+ import { log as log4 } from "@foxford/logger";
22
31
 
23
32
  // src/serve/assets.ts
24
33
  import { createReadStream, existsSync, statSync } from "fs";
@@ -79,7 +88,7 @@ function matchClaim(app, pathname) {
79
88
  }
80
89
  return (_b = (_a = matchRoute(app.routes, pathname)) == null ? void 0 : _a.params) != null ? _b : null;
81
90
  }
82
- function browserContext(req) {
91
+ function requestContextOf(req, fromBody) {
83
92
  const headers = {};
84
93
  const forwarded = {
85
94
  "accept-language": req.headers["x-forwarded-accept-language"],
@@ -91,22 +100,32 @@ function browserContext(req) {
91
100
  }
92
101
  }
93
102
  const cookie = req.headers.cookie;
94
- if (typeof cookie === "string" && cookie !== "") {
95
- headers.cookie = cookie;
103
+ const backendHeaders = __spreadValues(__spreadValues({}, typeof cookie === "string" && cookie !== "" ? { cookie } : {}), fromBody);
104
+ return { backendHeaders, cookies: parseCookieHeader(cookie), headers };
105
+ }
106
+ function backendHeadersOf(value) {
107
+ if (value === void 0) {
108
+ return void 0;
96
109
  }
97
- return { cookies: parseCookieHeader(cookie), headers };
110
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
111
+ return "invalid";
112
+ }
113
+ const entries = Object.entries(value);
114
+ if (entries.some(([, single]) => typeof single !== "string")) {
115
+ return "invalid";
116
+ }
117
+ return Object.fromEntries(entries.map(([name, single]) => [name.toLowerCase(), single]));
98
118
  }
99
119
  var appPlugin = (instance, options) => {
100
120
  var _a, _b;
101
121
  const { app, slowRenderMs = DEFAULT_SLOW_RENDER_MS } = options;
102
122
  const appLog = (_a = log.getLogger(app.name)) != null ? _a : logger;
103
123
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
104
- instance.get("/_health", () => "ok");
105
124
  instance.get("/_routes", (_req, reply) => {
106
125
  void reply.header("cache-control", "no-store");
107
126
  return claim(app);
108
127
  });
109
- function handleRender(req, reply, target, inputs) {
128
+ function handleRender(req, reply, target, inputs, rawBackendHeaders) {
110
129
  return __async(this, null, function* () {
111
130
  if (typeof target !== "string" || !target.startsWith("/")) {
112
131
  return reply.code(400).send({ error: "\u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 url" });
@@ -114,6 +133,10 @@ var appPlugin = (instance, options) => {
114
133
  if (inputs !== void 0 && (typeof inputs !== "object" || inputs === null || Array.isArray(inputs))) {
115
134
  return reply.code(400).send({ error: "inputs \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u043F\u043E \u043A\u043B\u044E\u0447\u0443 token.description" });
116
135
  }
136
+ const backendHeaders = backendHeadersOf(rawBackendHeaders);
137
+ if (backendHeaders === "invalid") {
138
+ return reply.code(400).send({ error: "backendHeaders \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \xAB\u0438\u043C\u044F \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u2192 \u0441\u0442\u0440\u043E\u043A\u0430\xBB" });
139
+ }
117
140
  const { pathname, search } = new URL(target, "http://localhost");
118
141
  const params = matchClaim(app, pathname);
119
142
  if (params === null) {
@@ -123,7 +146,7 @@ var appPlugin = (instance, options) => {
123
146
  const startedAt = Date.now();
124
147
  try {
125
148
  const result = yield renderApp(app, {
126
- context: browserContext(req),
149
+ context: requestContextOf(req, backendHeaders),
127
150
  inputs,
128
151
  params,
129
152
  pathname,
@@ -140,12 +163,15 @@ var appPlugin = (instance, options) => {
140
163
  }
141
164
  });
142
165
  }
143
- instance.get("/_render", (req, reply) => handleRender(req, reply, req.query.url, void 0));
166
+ instance.get(
167
+ "/_render",
168
+ (req, reply) => handleRender(req, reply, req.query.url, void 0, void 0)
169
+ );
144
170
  instance.route({
145
171
  handler: (req, reply) => {
146
172
  var _a2;
147
173
  const body = (_a2 = req.body) != null ? _a2 : {};
148
- return handleRender(req, reply, body.url, body.inputs);
174
+ return handleRender(req, reply, body.url, body.inputs, body.backendHeaders);
149
175
  },
150
176
  method: "QUERY",
151
177
  url: "/_render"
@@ -153,14 +179,235 @@ var appPlugin = (instance, options) => {
153
179
  return Promise.resolve();
154
180
  };
155
181
 
182
+ // src/serve/process.ts
183
+ import cluster from "cluster";
184
+ import { log as log3 } from "@foxford/logger";
185
+
186
+ // src/serve/worker.ts
187
+ import { log as log2 } from "@foxford/logger";
188
+ var ALIVE = "den:app-worker-alive";
189
+ var SIGNALS = ["SIGTERM", "SIGINT"];
190
+ var SHUTDOWN_MS = 1e4;
191
+ function startWorker(appName, start) {
192
+ return __async(this, null, function* () {
193
+ const aggregation = clusterMetrics();
194
+ const logger2 = log2.getLogger(appName).getLogger("cluster");
195
+ let timer;
196
+ let closing;
197
+ const starting = start();
198
+ const close = () => {
199
+ closing != null ? closing : closing = (() => __async(null, null, function* () {
200
+ clearInterval(timer);
201
+ try {
202
+ const instance = yield starting;
203
+ yield instance.close();
204
+ } finally {
205
+ try {
206
+ if (process.connected) yield aggregation.shutdown();
207
+ } finally {
208
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
209
+ process.removeListener("disconnect", shutdown);
210
+ if (process.connected) process.disconnect();
211
+ }
212
+ }
213
+ }))();
214
+ return closing;
215
+ };
216
+ const shutdown = () => {
217
+ const deadline = setTimeout(() => process.exit(1), SHUTDOWN_MS);
218
+ deadline.unref();
219
+ void close().then(() => process.exit(0)).catch((error) => {
220
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0432\u043E\u0440\u043A\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
221
+ process.exit(1);
222
+ });
223
+ };
224
+ for (const signal of SIGNALS) process.on(signal, shutdown);
225
+ process.once("disconnect", shutdown);
226
+ try {
227
+ yield starting;
228
+ if (!closing) {
229
+ const notify = () => {
230
+ var _a;
231
+ if (process.connected) (_a = process.send) == null ? void 0 : _a.call(process, { type: ALIVE });
232
+ };
233
+ timer = setInterval(notify, 2e3);
234
+ timer.unref();
235
+ notify();
236
+ }
237
+ } catch (error) {
238
+ yield close().catch(() => void 0);
239
+ throw error;
240
+ }
241
+ return { close };
242
+ });
243
+ }
244
+
245
+ // src/serve/process.ts
246
+ var STARTUP_MS = 6e4;
247
+ function startAppProcess(appName, options, start) {
248
+ return __async(this, null, function* () {
249
+ var _a;
250
+ const workers = (_a = options.workers) != null ? _a : 1;
251
+ if (!Number.isSafeInteger(workers) || workers < 1) {
252
+ throw new Error("WORKERS must be a positive integer");
253
+ }
254
+ if (workers === 1) {
255
+ const instance = yield start(options);
256
+ return { close: () => instance.close() };
257
+ }
258
+ const metrics = getProcessMetrics();
259
+ metrics.startRuntime(appName);
260
+ if (cluster.isWorker) {
261
+ return startWorker(appName, () => start(__spreadProps(__spreadValues({}, options), { gracefulShutdown: false, ops: false })));
262
+ }
263
+ return startPrimary(appName, options, workers);
264
+ });
265
+ }
266
+ function startPrimary(appName, options, workers) {
267
+ return __async(this, null, function* () {
268
+ const aggregation = clusterMetrics();
269
+ const logger2 = log3.getLogger(appName).getLogger("cluster");
270
+ const children = /* @__PURE__ */ new Set();
271
+ const alive = /* @__PURE__ */ new Map();
272
+ let ops;
273
+ let closing;
274
+ let started = false;
275
+ let resolveReady = () => void 0;
276
+ let rejectReady = () => void 0;
277
+ const ready = new Promise((resolve, reject) => {
278
+ resolveReady = resolve;
279
+ rejectReady = reject;
280
+ });
281
+ void ready.catch(() => void 0);
282
+ const timeout = setTimeout(() => rejectReady(new Error("Den workers startup timeout")), STARTUP_MS);
283
+ const onMessage = (worker, message) => {
284
+ if (children.has(worker) && typeof message === "object" && message !== null && "type" in message && message.type === ALIVE) {
285
+ alive.set(worker.id, Date.now());
286
+ if (alive.size === workers) resolveReady();
287
+ }
288
+ };
289
+ const close = () => {
290
+ closing != null ? closing : closing = (() => __async(null, null, function* () {
291
+ clearTimeout(timeout);
292
+ resolveReady();
293
+ cluster.removeListener("message", onMessage);
294
+ yield Promise.all([...children].map(stopWorker));
295
+ if (ops) yield closeHttpServer(ops);
296
+ yield aggregation.shutdown();
297
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
298
+ }))();
299
+ return closing;
300
+ };
301
+ const shutdown = () => {
302
+ void close().then(() => process.exit()).catch((error) => {
303
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043A\u043B\u0430\u0441\u0442\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
304
+ process.exit(1);
305
+ });
306
+ };
307
+ const failed = (error) => {
308
+ if (closing) return;
309
+ if (!started) {
310
+ rejectReady(error);
311
+ } else {
312
+ logger2.error(error);
313
+ process.exitCode = 1;
314
+ shutdown();
315
+ }
316
+ };
317
+ cluster.on("message", onMessage);
318
+ if (options.gracefulShutdown !== false) {
319
+ for (const signal of SIGNALS) process.on(signal, shutdown);
320
+ }
321
+ try {
322
+ if (options.ops !== false) {
323
+ const supplied = options.ops;
324
+ ops = yield startOpsServer(__spreadProps(__spreadValues({}, supplied), {
325
+ collect: aggregation.collect,
326
+ contentType: aggregation.contentType,
327
+ isLive: () => {
328
+ var _a;
329
+ return [...children].some((worker) => {
330
+ var _a2;
331
+ return worker.isConnected() && Date.now() - ((_a2 = alive.get(worker.id)) != null ? _a2 : 0) < 1e4;
332
+ }) && ((_a = supplied == null ? void 0 : supplied.isLive) == null ? void 0 : _a.call(supplied)) !== false;
333
+ }
334
+ }));
335
+ }
336
+ if (closing) {
337
+ if (ops) yield closeHttpServer(ops);
338
+ yield closing;
339
+ return { close };
340
+ }
341
+ for (let i = 0; i < workers; i++) {
342
+ const worker = cluster.fork();
343
+ children.add(worker);
344
+ worker.once("error", failed);
345
+ worker.once("exit", (code, signal) => {
346
+ alive.delete(worker.id);
347
+ failed(new Error(`Den worker ${String(worker.process.pid)} exited (${String(code != null ? code : signal)})`));
348
+ });
349
+ }
350
+ yield ready;
351
+ started = true;
352
+ clearTimeout(timeout);
353
+ if (!closing) logger2.info(`\u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u0432\u043E\u0440\u043A\u0435\u0440\u043E\u0432: ${String(workers)}`);
354
+ return { close };
355
+ } catch (error) {
356
+ yield close();
357
+ throw error;
358
+ }
359
+ });
360
+ }
361
+ function stopWorker(worker) {
362
+ return __async(this, null, function* () {
363
+ if (worker.isDead()) return;
364
+ yield new Promise((resolve) => {
365
+ const timeout = setTimeout(() => worker.kill("SIGKILL"), SHUTDOWN_MS);
366
+ worker.once("exit", () => {
367
+ clearTimeout(timeout);
368
+ resolve();
369
+ });
370
+ worker.kill("SIGTERM");
371
+ });
372
+ });
373
+ }
374
+
156
375
  // src/serve/index.ts
376
+ function serveAppProcess(app, options = {}) {
377
+ return startAppProcess(app.name, options, (serveOptions) => serveApp(app, serveOptions));
378
+ }
157
379
  var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
158
380
  function serveApp(_0) {
159
381
  return __async(this, arguments, function* (app, options = {}) {
160
- var _a, _b, _c, _d;
161
- const appLog = (_a = log2.getLogger(app.name)) != null ? _a : logger;
382
+ var _a, _b, _c, _d, _e;
383
+ const appLog = (_a = log4.getLogger(app.name)) != null ? _a : logger;
162
384
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
163
385
  const instance = Fastify({ logger: false });
386
+ const metrics = getProcessMetrics();
387
+ metrics.startRuntime(app.name);
388
+ let ops;
389
+ const shutdown = () => {
390
+ void instance.close();
391
+ };
392
+ instance.addHook("onRequest", (req, reply, done) => {
393
+ if (isOpsPath(req.url)) {
394
+ void reply.code(404).send();
395
+ return;
396
+ }
397
+ metrics.trackHttp(req.raw, reply.raw, () => {
398
+ var _a2;
399
+ return (_a2 = req.routeOptions.url) != null ? _a2 : "unmatched";
400
+ });
401
+ done();
402
+ });
403
+ instance.addHook("onClose", () => __async(null, null, function* () {
404
+ for (const signal of SHUTDOWN_SIGNALS) {
405
+ process.removeListener(signal, shutdown);
406
+ }
407
+ if (ops) {
408
+ yield closeHttpServer(ops);
409
+ }
410
+ }));
164
411
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
165
412
  if (options.assets) {
166
413
  registerAssets(instance, options.assets);
@@ -174,14 +421,19 @@ function serveApp(_0) {
174
421
  serveLog.info(`\u2190 ${String(reply.statusCode)} ${req.method} ${req.url} ${elapsed}\u043C\u0441`);
175
422
  done();
176
423
  });
177
- yield (_c = options.configure) == null ? void 0 : _c.call(options, instance);
178
- yield instance.listen({ host: options.host, port: (_d = options.port) != null ? _d : 0 });
424
+ try {
425
+ yield (_c = options.configure) == null ? void 0 : _c.call(options, instance);
426
+ if (options.ops !== false) {
427
+ ops = yield startOpsServer(options.ops);
428
+ }
429
+ yield instance.listen({ host: (_d = options.host) != null ? _d : "0.0.0.0", port: (_e = options.port) != null ? _e : 8080 });
430
+ } catch (error) {
431
+ yield instance.close();
432
+ throw error;
433
+ }
179
434
  if (options.gracefulShutdown !== false) {
180
435
  for (const signal of SHUTDOWN_SIGNALS) {
181
- process.once(signal, () => {
182
- serveLog.info(`${signal}: \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0438\u043D\u0441\u0442\u0430\u043D\u0441`);
183
- void instance.close();
184
- });
436
+ process.once(signal, shutdown);
185
437
  }
186
438
  }
187
439
  serveLog.info(
@@ -194,5 +446,6 @@ export {
194
446
  appPlugin,
195
447
  loadAppEnv,
196
448
  registerAssets,
197
- serveApp
449
+ serveApp,
450
+ serveAppProcess
198
451
  };
@@ -477,14 +477,33 @@ declare class StateRegistry {
477
477
  }
478
478
 
479
479
  /**
480
- * Контекст HTTP-запроса — заголовки и cookies, переданные сервером.
481
- * На клиенте содержит пустые объекты.
480
+ * Реквизиты HTTP-запроса. Три поля отвечают на ТРИ РАЗНЫХ вопроса, и путать их нельзя:
481
+ * что прислал браузер, чему равна отдельная кука и что предъявлять бэкенду.
482
+ *
483
+ * На клиенте заголовков нет, куки берутся из документа, а реквизиты бэкенда подставляет
484
+ * сам браузер.
482
485
  */
483
486
  interface RequestContext {
484
- /** HTTP-заголовки запроса (в нижнем регистре) */
487
+ /**
488
+ * Что прислал БРАУЗЕР: `host`, `accept-language` (в нижнем регистре).
489
+ *
490
+ * Читается для решений о рендере — собрать абсолютную ссылку, выбрать язык. Заголовка
491
+ * `cookie` здесь НЕТ намеренно: собери кто-нибудь по нему запрос — он отправил бы браузерную
492
+ * куку вместо реквизитов этого запроса, и на первом визите это был бы другой посетитель.
493
+ * Нужно значение куки — есть `cookies`; нужно сходить в бэкенд — есть `backendHeaders`.
494
+ */
485
495
  headers: Record<string, string>;
486
- /** Распарсенные cookies из заголовка Cookie */
496
+ /** Разобранные куки — чтобы прочитать значение конкретной куки. */
487
497
  cookies: Record<string, string>;
498
+ /**
499
+ * Что предъявлять БЭКЕНДУ в каждом запросе: `cookie` и то, что бэкенд выдал хосту на
500
+ * бутстрапе своими заголовками (идентичность посетителя и прочее).
501
+ *
502
+ * Один набор на все походы приложения, и собирает его хост — до рендера и один раз.
503
+ * Навешивает на клиент расширение рантайма `@foxford/den-sdk`, поэтому прикладной код
504
+ * заголовков не касается вовсе.
505
+ */
506
+ backendHeaders?: Record<string, string>;
488
507
  }
489
508
  /**
490
509
  * Результат проверки guard.
@@ -477,14 +477,33 @@ declare class StateRegistry {
477
477
  }
478
478
 
479
479
  /**
480
- * Контекст HTTP-запроса — заголовки и cookies, переданные сервером.
481
- * На клиенте содержит пустые объекты.
480
+ * Реквизиты HTTP-запроса. Три поля отвечают на ТРИ РАЗНЫХ вопроса, и путать их нельзя:
481
+ * что прислал браузер, чему равна отдельная кука и что предъявлять бэкенду.
482
+ *
483
+ * На клиенте заголовков нет, куки берутся из документа, а реквизиты бэкенда подставляет
484
+ * сам браузер.
482
485
  */
483
486
  interface RequestContext {
484
- /** HTTP-заголовки запроса (в нижнем регистре) */
487
+ /**
488
+ * Что прислал БРАУЗЕР: `host`, `accept-language` (в нижнем регистре).
489
+ *
490
+ * Читается для решений о рендере — собрать абсолютную ссылку, выбрать язык. Заголовка
491
+ * `cookie` здесь НЕТ намеренно: собери кто-нибудь по нему запрос — он отправил бы браузерную
492
+ * куку вместо реквизитов этого запроса, и на первом визите это был бы другой посетитель.
493
+ * Нужно значение куки — есть `cookies`; нужно сходить в бэкенд — есть `backendHeaders`.
494
+ */
485
495
  headers: Record<string, string>;
486
- /** Распарсенные cookies из заголовка Cookie */
496
+ /** Разобранные куки — чтобы прочитать значение конкретной куки. */
487
497
  cookies: Record<string, string>;
498
+ /**
499
+ * Что предъявлять БЭКЕНДУ в каждом запросе: `cookie` и то, что бэкенд выдал хосту на
500
+ * бутстрапе своими заголовками (идентичность посетителя и прочее).
501
+ *
502
+ * Один набор на все походы приложения, и собирает его хост — до рендера и один раз.
503
+ * Навешивает на клиент расширение рантайма `@foxford/den-sdk`, поэтому прикладной код
504
+ * заголовков не касается вовсе.
505
+ */
506
+ backendHeaders?: Record<string, string>;
488
507
  }
489
508
  /**
490
509
  * Результат проверки guard.
@@ -1,6 +1,6 @@
1
- import { I as IslandDescriptor } from './descriptor-C2j6Zz6c.cjs';
1
+ import { I as IslandDescriptor } from './descriptor-DmfJP4jQ.cjs';
2
2
  import { S as ServerRenderResult } from './server-render-CocWXQKC.cjs';
3
- import { L as LayerDefinition, h as LayerConfig } from './types-BxMQ-VOd.cjs';
3
+ import { L as LayerDefinition, h as LayerConfig } from './types-Ccd5bmt1.cjs';
4
4
  import { Container, Token } from '@foxford/ioc';
5
5
 
6
6
  /** Что нужно адаптеру, чтобы отрисовать остров на сервере. */
@@ -1,6 +1,6 @@
1
- import { I as IslandDescriptor } from './descriptor-BSaHM5B4.js';
1
+ import { I as IslandDescriptor } from './descriptor-CNJFrEbS.js';
2
2
  import { S as ServerRenderResult } from './server-render-CocWXQKC.js';
3
- import { L as LayerDefinition, h as LayerConfig } from './types-BxMQ-VOd.js';
3
+ import { L as LayerDefinition, h as LayerConfig } from './types-Ccd5bmt1.js';
4
4
  import { Container, Token } from '@foxford/ioc';
5
5
 
6
6
  /** Что нужно адаптеру, чтобы отрисовать остров на сервере. */