@nuraly/lumenjs 0.1.3 → 0.1.4

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 (333) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +73 -0
  11. package/dist/auth/native-auth.js +293 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +98 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/utils.d.ts +7 -0
  31. package/dist/auth/routes/utils.js +35 -0
  32. package/dist/auth/routes/verify.d.ts +3 -0
  33. package/dist/auth/routes/verify.js +26 -0
  34. package/dist/auth/routes.d.ts +8 -0
  35. package/dist/auth/routes.js +110 -0
  36. package/dist/auth/session.d.ts +8 -0
  37. package/dist/auth/session.js +54 -0
  38. package/dist/auth/token.d.ts +33 -0
  39. package/dist/auth/token.js +90 -0
  40. package/dist/auth/types.d.ts +156 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/build/build-client.d.ts +15 -0
  43. package/dist/build/build-client.js +45 -0
  44. package/dist/build/build-prerender.d.ts +11 -0
  45. package/dist/build/build-prerender.js +159 -0
  46. package/dist/build/build-server.d.ts +17 -0
  47. package/dist/build/build-server.js +98 -0
  48. package/dist/build/build.js +48 -120
  49. package/dist/build/scan.d.ts +17 -0
  50. package/dist/build/scan.js +76 -6
  51. package/dist/build/serve-api.js +8 -2
  52. package/dist/build/serve-loaders.d.ts +4 -4
  53. package/dist/build/serve-loaders.js +26 -18
  54. package/dist/build/serve-ssr.js +38 -11
  55. package/dist/build/serve-static.js +3 -3
  56. package/dist/build/serve.js +218 -15
  57. package/dist/cli.js +37 -6
  58. package/dist/communication/encryption.d.ts +35 -0
  59. package/dist/communication/encryption.js +90 -0
  60. package/dist/communication/handlers/context.d.ts +27 -0
  61. package/dist/communication/handlers/context.js +1 -0
  62. package/dist/communication/handlers/conversation.d.ts +24 -0
  63. package/dist/communication/handlers/conversation.js +113 -0
  64. package/dist/communication/handlers/file-upload.d.ts +17 -0
  65. package/dist/communication/handlers/file-upload.js +62 -0
  66. package/dist/communication/handlers/messaging.d.ts +30 -0
  67. package/dist/communication/handlers/messaging.js +237 -0
  68. package/dist/communication/handlers/presence.d.ts +15 -0
  69. package/dist/communication/handlers/presence.js +76 -0
  70. package/dist/communication/handlers.d.ts +5 -0
  71. package/dist/communication/handlers.js +5 -0
  72. package/dist/communication/index.d.ts +9 -0
  73. package/dist/communication/index.js +7 -0
  74. package/dist/communication/link-preview.d.ts +18 -0
  75. package/dist/communication/link-preview.js +115 -0
  76. package/dist/communication/schema.d.ts +10 -0
  77. package/dist/communication/schema.js +101 -0
  78. package/dist/communication/server.d.ts +86 -0
  79. package/dist/communication/server.js +212 -0
  80. package/dist/communication/signaling.d.ts +43 -0
  81. package/dist/communication/signaling.js +271 -0
  82. package/dist/communication/store.d.ts +71 -0
  83. package/dist/communication/store.js +289 -0
  84. package/dist/communication/types.d.ts +454 -0
  85. package/dist/communication/types.js +1 -0
  86. package/dist/create.d.ts +1 -0
  87. package/dist/create.js +55 -0
  88. package/dist/db/auto-migrate.d.ts +3 -0
  89. package/dist/db/auto-migrate.js +100 -0
  90. package/dist/db/client.d.ts +3 -0
  91. package/dist/db/client.js +18 -0
  92. package/dist/db/index.d.ts +17 -13
  93. package/dist/db/index.js +205 -26
  94. package/dist/db/seed.d.ts +12 -0
  95. package/dist/db/seed.js +88 -0
  96. package/dist/db/table.d.ts +10 -0
  97. package/dist/db/table.js +12 -0
  98. package/dist/dev-server/config.d.ts +11 -0
  99. package/dist/dev-server/config.js +23 -20
  100. package/dist/dev-server/index-html.d.ts +3 -0
  101. package/dist/dev-server/index-html.js +18 -6
  102. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  103. package/dist/dev-server/nuralyui-aliases.js +115 -94
  104. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  105. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  106. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  107. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  108. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  109. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  110. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  111. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  113. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  114. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  115. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  116. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  117. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  118. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  119. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  120. package/dist/dev-server/plugins/vite-plugin-routes.js +15 -5
  121. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  122. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  123. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  125. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  126. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  127. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
  128. package/dist/dev-server/server.js +127 -13
  129. package/dist/dev-server/ssr-render.d.ts +2 -1
  130. package/dist/dev-server/ssr-render.js +107 -48
  131. package/dist/editor/ai/backend.d.ts +20 -0
  132. package/dist/editor/ai/backend.js +104 -0
  133. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  134. package/dist/editor/ai/claude-code-client.js +145 -0
  135. package/dist/editor/ai/opencode-client.d.ts +14 -0
  136. package/dist/editor/ai/opencode-client.js +125 -0
  137. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  138. package/dist/editor/ai/snapshot-store.js +35 -0
  139. package/dist/editor/ai/types.d.ts +30 -0
  140. package/dist/editor/ai/types.js +136 -0
  141. package/dist/editor/ai-chat-panel.d.ts +13 -0
  142. package/dist/editor/ai-chat-panel.js +587 -0
  143. package/dist/editor/ai-markdown.d.ts +10 -0
  144. package/dist/editor/ai-markdown.js +70 -0
  145. package/dist/editor/ai-project-panel.d.ts +11 -0
  146. package/dist/editor/ai-project-panel.js +332 -0
  147. package/dist/editor/ast-modification.d.ts +11 -0
  148. package/dist/editor/ast-modification.js +1 -0
  149. package/dist/editor/ast-service.d.ts +30 -0
  150. package/dist/editor/ast-service.js +180 -0
  151. package/dist/editor/css-rules.d.ts +54 -0
  152. package/dist/editor/css-rules.js +423 -0
  153. package/dist/editor/editor-api-client.d.ts +51 -0
  154. package/dist/editor/editor-api-client.js +162 -0
  155. package/dist/editor/editor-bridge.d.ts +1 -0
  156. package/dist/editor/editor-bridge.js +17 -8
  157. package/dist/editor/editor-toolbar.d.ts +14 -0
  158. package/dist/editor/editor-toolbar.js +115 -0
  159. package/dist/editor/file-editor.d.ts +9 -0
  160. package/dist/editor/file-editor.js +236 -0
  161. package/dist/editor/file-service.d.ts +16 -0
  162. package/dist/editor/file-service.js +52 -0
  163. package/dist/editor/i18n-key-gen.d.ts +1 -0
  164. package/dist/editor/i18n-key-gen.js +7 -0
  165. package/dist/editor/inline-text-edit.d.ts +5 -0
  166. package/dist/editor/inline-text-edit.js +173 -92
  167. package/dist/editor/overlay-events.d.ts +5 -0
  168. package/dist/editor/overlay-events.js +364 -0
  169. package/dist/editor/overlay-hmr.d.ts +2 -0
  170. package/dist/editor/overlay-hmr.js +75 -0
  171. package/dist/editor/overlay-selection.d.ts +29 -0
  172. package/dist/editor/overlay-selection.js +148 -0
  173. package/dist/editor/overlay-utils.d.ts +12 -0
  174. package/dist/editor/overlay-utils.js +59 -0
  175. package/dist/editor/properties-panel-persist.d.ts +14 -0
  176. package/dist/editor/properties-panel-persist.js +70 -0
  177. package/dist/editor/properties-panel-rows.d.ts +10 -0
  178. package/dist/editor/properties-panel-rows.js +349 -0
  179. package/dist/editor/properties-panel-styles.d.ts +4 -0
  180. package/dist/editor/properties-panel-styles.js +174 -0
  181. package/dist/editor/properties-panel.d.ts +4 -0
  182. package/dist/editor/properties-panel.js +148 -0
  183. package/dist/editor/property-registry.d.ts +16 -0
  184. package/dist/editor/property-registry.js +303 -0
  185. package/dist/editor/standalone-file-panel.d.ts +0 -0
  186. package/dist/editor/standalone-file-panel.js +1 -0
  187. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  188. package/dist/editor/standalone-overlay-dom.js +1 -0
  189. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  190. package/dist/editor/standalone-overlay-styles.js +1 -0
  191. package/dist/editor/standalone-overlay.d.ts +1 -0
  192. package/dist/editor/standalone-overlay.js +76 -0
  193. package/dist/editor/syntax-highlighter.d.ts +4 -0
  194. package/dist/editor/syntax-highlighter.js +81 -0
  195. package/dist/editor/text-toolbar.d.ts +11 -0
  196. package/dist/editor/text-toolbar.js +327 -0
  197. package/dist/editor/toolbar-styles.d.ts +4 -0
  198. package/dist/editor/toolbar-styles.js +198 -0
  199. package/dist/email/index.d.ts +32 -0
  200. package/dist/email/index.js +154 -0
  201. package/dist/email/providers/resend.d.ts +2 -0
  202. package/dist/email/providers/resend.js +24 -0
  203. package/dist/email/providers/sendgrid.d.ts +2 -0
  204. package/dist/email/providers/sendgrid.js +31 -0
  205. package/dist/email/providers/smtp.d.ts +13 -0
  206. package/dist/email/providers/smtp.js +125 -0
  207. package/dist/email/template-engine.d.ts +18 -0
  208. package/dist/email/template-engine.js +116 -0
  209. package/dist/email/templates/base.d.ts +9 -0
  210. package/dist/email/templates/base.js +65 -0
  211. package/dist/email/templates/password-reset.d.ts +5 -0
  212. package/dist/email/templates/password-reset.js +15 -0
  213. package/dist/email/templates/verify-email.d.ts +5 -0
  214. package/dist/email/templates/verify-email.js +15 -0
  215. package/dist/email/templates/welcome.d.ts +5 -0
  216. package/dist/email/templates/welcome.js +13 -0
  217. package/dist/email/types.d.ts +49 -0
  218. package/dist/email/types.js +1 -0
  219. package/dist/llms/generate.d.ts +46 -0
  220. package/dist/llms/generate.js +185 -0
  221. package/dist/permissions/guard.d.ts +28 -0
  222. package/dist/permissions/guard.js +30 -0
  223. package/dist/permissions/index.d.ts +6 -0
  224. package/dist/permissions/index.js +3 -0
  225. package/dist/permissions/service.d.ts +80 -0
  226. package/dist/permissions/service.js +210 -0
  227. package/dist/permissions/tables.d.ts +5 -0
  228. package/dist/permissions/tables.js +68 -0
  229. package/dist/permissions/types.d.ts +33 -0
  230. package/dist/permissions/types.js +1 -0
  231. package/dist/runtime/app-shell.js +163 -0
  232. package/dist/runtime/auth.d.ts +10 -0
  233. package/dist/runtime/auth.js +30 -0
  234. package/dist/runtime/communication.d.ts +137 -0
  235. package/dist/runtime/communication.js +228 -0
  236. package/dist/runtime/error-boundary.d.ts +23 -0
  237. package/dist/runtime/error-boundary.js +120 -0
  238. package/dist/runtime/i18n.d.ts +6 -1
  239. package/dist/runtime/i18n.js +42 -21
  240. package/dist/runtime/router-data.d.ts +3 -0
  241. package/dist/runtime/router-data.js +102 -17
  242. package/dist/runtime/router-hydration.js +25 -0
  243. package/dist/runtime/router.d.ts +16 -1
  244. package/dist/runtime/router.js +188 -42
  245. package/dist/runtime/socket-client.d.ts +2 -0
  246. package/dist/runtime/socket-client.js +30 -0
  247. package/dist/runtime/webrtc.d.ts +47 -0
  248. package/dist/runtime/webrtc.js +178 -0
  249. package/dist/shared/graceful-shutdown.d.ts +8 -0
  250. package/dist/shared/graceful-shutdown.js +36 -0
  251. package/dist/shared/health.d.ts +8 -0
  252. package/dist/shared/health.js +25 -0
  253. package/dist/shared/llms-txt.d.ts +31 -0
  254. package/dist/shared/llms-txt.js +85 -0
  255. package/dist/shared/logger.d.ts +32 -0
  256. package/dist/shared/logger.js +93 -0
  257. package/dist/shared/meta.d.ts +27 -0
  258. package/dist/shared/meta.js +71 -0
  259. package/dist/shared/middleware-runner.d.ts +9 -0
  260. package/dist/shared/middleware-runner.js +29 -0
  261. package/dist/shared/rate-limit.d.ts +18 -0
  262. package/dist/shared/rate-limit.js +71 -0
  263. package/dist/shared/request-id.d.ts +5 -0
  264. package/dist/shared/request-id.js +18 -0
  265. package/dist/shared/route-matching.js +16 -1
  266. package/dist/shared/security-headers.d.ts +18 -0
  267. package/dist/shared/security-headers.js +38 -0
  268. package/dist/shared/socket-io-setup.d.ts +11 -0
  269. package/dist/shared/socket-io-setup.js +51 -0
  270. package/dist/shared/types.d.ts +14 -0
  271. package/dist/shared/utils.d.ts +33 -7
  272. package/dist/shared/utils.js +164 -27
  273. package/dist/storage/adapters/local.d.ts +44 -0
  274. package/dist/storage/adapters/local.js +85 -0
  275. package/dist/storage/adapters/s3.d.ts +32 -0
  276. package/dist/storage/adapters/s3.js +116 -0
  277. package/dist/storage/adapters/types.d.ts +53 -0
  278. package/dist/storage/adapters/types.js +1 -0
  279. package/dist/storage/index.d.ts +76 -0
  280. package/dist/storage/index.js +83 -0
  281. package/package.json +19 -7
  282. package/templates/blog/api/posts.ts +4 -18
  283. package/templates/blog/data/migrations/001_init.sql +6 -5
  284. package/templates/blog/lumenjs.config.ts +3 -0
  285. package/templates/blog/package.json +14 -0
  286. package/templates/blog/pages/_layout.ts +25 -0
  287. package/templates/blog/pages/index.ts +48 -22
  288. package/templates/blog/pages/posts/[slug].ts +45 -20
  289. package/templates/blog/pages/tag/[tag].ts +44 -0
  290. package/templates/dashboard/api/stats.ts +8 -5
  291. package/templates/dashboard/lumenjs.config.ts +3 -0
  292. package/templates/dashboard/package.json +14 -0
  293. package/templates/dashboard/pages/_layout.ts +25 -0
  294. package/templates/dashboard/pages/index.ts +54 -23
  295. package/templates/dashboard/pages/settings/index.ts +29 -0
  296. package/templates/default/lumenjs.config.ts +3 -0
  297. package/templates/default/package.json +14 -0
  298. package/templates/default/pages/index.ts +24 -0
  299. package/templates/social/api/posts/[id].ts +14 -0
  300. package/templates/social/api/posts.ts +11 -0
  301. package/templates/social/api/profile/[username].ts +10 -0
  302. package/templates/social/api/upload.ts +19 -0
  303. package/templates/social/data/migrations/001_init.sql +78 -0
  304. package/templates/social/data/migrations/002_add_image_url.sql +1 -0
  305. package/templates/social/data/migrations/003_auth.sql +7 -0
  306. package/templates/social/docs/architecture.md +76 -0
  307. package/templates/social/docs/components.md +100 -0
  308. package/templates/social/docs/data.md +89 -0
  309. package/templates/social/docs/pages.md +96 -0
  310. package/templates/social/docs/theming.md +52 -0
  311. package/templates/social/lib/media.ts +130 -0
  312. package/templates/social/lumenjs.auth.ts +21 -0
  313. package/templates/social/lumenjs.config.ts +3 -0
  314. package/templates/social/package.json +5 -0
  315. package/templates/social/pages/_layout.ts +239 -0
  316. package/templates/social/pages/apps/[id].ts +173 -0
  317. package/templates/social/pages/apps/index.ts +116 -0
  318. package/templates/social/pages/auth/login.ts +92 -0
  319. package/templates/social/pages/bookmarks.ts +57 -0
  320. package/templates/social/pages/explore.ts +73 -0
  321. package/templates/social/pages/index.ts +351 -0
  322. package/templates/social/pages/messages.ts +298 -0
  323. package/templates/social/pages/new.ts +77 -0
  324. package/templates/social/pages/notifications.ts +73 -0
  325. package/templates/social/pages/post/[id].ts +124 -0
  326. package/templates/social/pages/profile/[username].ts +100 -0
  327. package/templates/social/pages/settings/accessibility.ts +153 -0
  328. package/templates/social/pages/settings/account.ts +260 -0
  329. package/templates/social/pages/settings/help.ts +141 -0
  330. package/templates/social/pages/settings/language.ts +103 -0
  331. package/templates/social/pages/settings/privacy.ts +183 -0
  332. package/templates/social/pages/settings/security.ts +133 -0
  333. package/templates/social/pages/settings.ts +185 -0
@@ -1,6 +1,6 @@
1
- import { fetchLoaderData, fetchLayoutLoaderData, connectSubscribe, connectLayoutSubscribe, render404 } from './router-data.js';
1
+ import { fetchLoaderData, fetchLayoutLoaderData, prefetchLoaderData, prefetchLayoutLoaderData, connectSubscribe, connectLayoutSubscribe, render404 } from './router-data.js';
2
2
  import { hydrateInitialRoute } from './router-hydration.js';
3
- import { getI18nConfig, getLocale, stripLocalePrefix, buildLocalePath } from './i18n.js';
3
+ import { getI18nConfig, getLocale, initI18n, stripLocalePrefix, buildLocalePath } from './i18n.js';
4
4
  /**
5
5
  * Simple client-side router for LumenJS pages.
6
6
  * Handles popstate and link clicks for SPA navigation.
@@ -15,25 +15,76 @@ export class NkRouter {
15
15
  this.subscriptions = [];
16
16
  this.params = {};
17
17
  this.outlet = outlet;
18
+ this.siteTitle = document.title || 'LumenJS App';
18
19
  this.routes = routes.map(r => ({
19
20
  ...r,
20
21
  ...this.compilePattern(r.path),
21
22
  }));
23
+ // Initialize i18n from inlined data before any rendering
24
+ const i18nScript = document.getElementById('__nk_i18n__');
25
+ if (i18nScript) {
26
+ try {
27
+ const i18nData = JSON.parse(i18nScript.textContent || '');
28
+ initI18n(i18nData.config, i18nData.locale, i18nData.translations);
29
+ }
30
+ catch { /* ignore */ }
31
+ if (!hydrate)
32
+ i18nScript.remove();
33
+ }
22
34
  window.addEventListener('popstate', () => {
23
35
  const path = this.stripLocale(location.pathname);
24
36
  this.navigate(path, false);
25
37
  });
38
+ // Re-run loader when page is restored from bfcache (back/forward on mobile Safari, etc.)
39
+ window.addEventListener('pageshow', (e) => {
40
+ if (e.persisted) {
41
+ const path = this.stripLocale(location.pathname);
42
+ this.navigate(path, false);
43
+ }
44
+ });
26
45
  document.addEventListener('click', (e) => this.handleLinkClick(e));
46
+ window.__nk_navigate = (href) => {
47
+ const path = this.stripLocale(href);
48
+ if (this.matchRoute(path.split('?')[0])) {
49
+ this.navigate(path);
50
+ }
51
+ else {
52
+ window.location.href = href;
53
+ }
54
+ };
55
+ window.__nk_prefetch = (href) => {
56
+ const path = this.stripLocale(href);
57
+ this.prefetch(path);
58
+ };
27
59
  if (hydrate) {
28
60
  hydrateInitialRoute(this.routes, this.outlet, (p) => this.matchRoute(p), (tag, layoutTags, params) => {
29
61
  this.currentTag = tag;
30
62
  this.currentLayoutTags = layoutTags;
31
63
  this.params = params;
32
64
  });
65
+ // Wire up SSE subscriptions after hydration
66
+ const path = this.stripLocale(location.pathname);
67
+ this.setupSubscriptions(path);
33
68
  }
34
69
  else {
35
- const path = this.stripLocale(location.pathname);
36
- this.navigate(path, false);
70
+ // Initialize auth from inlined data before navigating (CSR path)
71
+ const authScript = document.getElementById('__nk_auth__');
72
+ if (authScript) {
73
+ import('@lumenjs/auth').then(({ initAuth }) => {
74
+ try {
75
+ initAuth(JSON.parse(authScript.textContent || ''));
76
+ }
77
+ catch { }
78
+ authScript.remove();
79
+ }).catch(() => { }).finally(() => {
80
+ const path = this.stripLocale(location.pathname);
81
+ this.navigate(path, false);
82
+ });
83
+ }
84
+ else {
85
+ const path = this.stripLocale(location.pathname);
86
+ this.navigate(path, false);
87
+ }
37
88
  }
38
89
  }
39
90
  compilePattern(path) {
@@ -50,8 +101,9 @@ export class NkRouter {
50
101
  }
51
102
  this.subscriptions = [];
52
103
  }
53
- async navigate(pathname, pushState = true) {
104
+ async navigate(fullPath, pushState = true) {
54
105
  this.cleanupSubscriptions();
106
+ const pathname = fullPath.split('?')[0];
55
107
  const match = this.matchRoute(pathname);
56
108
  if (!match) {
57
109
  if (this.outlet)
@@ -61,51 +113,53 @@ export class NkRouter {
61
113
  return;
62
114
  }
63
115
  if (pushState) {
64
- const localePath = this.withLocale(pathname);
116
+ const localePath = this.withLocale(fullPath);
65
117
  history.pushState(null, '', localePath);
66
118
  window.scrollTo(0, 0);
67
119
  }
68
120
  this.params = match.params;
69
- // Lazy-load the page component if not yet registered
70
- if (match.route.load && !customElements.get(match.route.tagName)) {
71
- await match.route.load();
72
- }
73
- // Load layout components
74
- const layouts = match.route.layouts || [];
75
- for (const layout of layouts) {
76
- if (layout.load && !customElements.get(layout.tagName)) {
77
- await layout.load();
78
- }
79
- }
80
- // Fetch loader data for page
81
- let loaderData = undefined;
82
- if (match.route.hasLoader) {
121
+ // Auth guard: SPA-navigate unauthenticated users to login page
122
+ if (match.route.__nk_has_auth) {
83
123
  try {
84
- loaderData = await fetchLoaderData(pathname, match.params);
85
- }
86
- catch (err) {
87
- console.error('[NkRouter] Loader fetch failed:', err);
88
- }
89
- }
90
- // Fetch loader data for layouts
91
- const layoutDataList = [];
92
- for (const layout of layouts) {
93
- if (layout.hasLoader) {
94
- try {
95
- const data = await fetchLayoutLoaderData(layout.loaderPath || '');
96
- layoutDataList.push(data);
124
+ const { isAuthenticated } = await import('@lumenjs/auth');
125
+ if (!isAuthenticated()) {
126
+ const loginPath = '/auth/login';
127
+ const loginUrl = `${loginPath}?returnTo=${encodeURIComponent(pathname)}`;
128
+ history.pushState(null, '', loginUrl);
129
+ this.navigate(loginPath, false);
130
+ return;
97
131
  }
98
- catch (err) {
99
- console.error('[NkRouter] Layout loader fetch failed:', err);
100
- layoutDataList.push(undefined);
101
- }
102
- }
103
- else {
104
- layoutDataList.push(undefined);
105
132
  }
133
+ catch { }
106
134
  }
135
+ const layouts = match.route.layouts || [];
136
+ // Load all component JS chunks in parallel
137
+ await Promise.all([
138
+ match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
139
+ ...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
140
+ ]);
141
+ // Fetch all loader data in parallel
142
+ const loaderPromises = [
143
+ match.route.hasLoader
144
+ ? fetchLoaderData(pathname, match.params).catch(err => { console.error('[NkRouter] Loader fetch failed:', err); return undefined; })
145
+ : Promise.resolve(undefined),
146
+ ...layouts.map(layout => layout.hasLoader
147
+ ? fetchLayoutLoaderData(layout.loaderPath || '').catch(err => { console.error('[NkRouter] Layout loader fetch failed:', err); return undefined; })
148
+ : Promise.resolve(undefined)),
149
+ ];
150
+ const [loaderData, ...layoutDataList] = await Promise.all(loaderPromises);
107
151
  this.renderRoute(match.route, loaderData, layouts, layoutDataList);
108
- // Set up SSE subscriptions for page
152
+ // Update document.title and announce route change for screen readers
153
+ this.updatePageMeta(match.route, loaderData);
154
+ // Set up SSE subscriptions
155
+ this.setupSubscriptions(pathname);
156
+ }
157
+ setupSubscriptions(pathname) {
158
+ const match = this.matchRoute(pathname);
159
+ if (!match)
160
+ return;
161
+ const layouts = match.route.layouts || [];
162
+ // Page subscription
109
163
  if (match.route.hasSubscribe) {
110
164
  const es = connectSubscribe(pathname, match.params);
111
165
  es.onmessage = (e) => {
@@ -115,7 +169,7 @@ export class NkRouter {
115
169
  };
116
170
  this.subscriptions.push(es);
117
171
  }
118
- // Set up SSE subscriptions for layouts
172
+ // Layout subscriptions
119
173
  for (const layout of layouts) {
120
174
  if (layout.hasSubscribe) {
121
175
  const es = connectLayoutSubscribe(layout.loaderPath || '');
@@ -177,6 +231,7 @@ export class NkRouter {
177
231
  }
178
232
  if (layoutDataList && layoutDataList[i] !== undefined) {
179
233
  layoutEl.loaderData = layoutDataList[i];
234
+ this.spreadData(layoutEl, layoutDataList[i]);
180
235
  }
181
236
  parentEl = layoutEl;
182
237
  }
@@ -201,12 +256,14 @@ export class NkRouter {
201
256
  const outerLayout = document.createElement(layouts[0].tagName);
202
257
  if (layoutDataList[0] !== undefined) {
203
258
  outerLayout.loaderData = layoutDataList[0];
259
+ this.spreadData(outerLayout, layoutDataList[0]);
204
260
  }
205
261
  let current = outerLayout;
206
262
  for (let i = 1; i < layouts.length; i++) {
207
263
  const inner = document.createElement(layouts[i].tagName);
208
264
  if (layoutDataList[i] !== undefined) {
209
265
  inner.loaderData = layoutDataList[i];
266
+ this.spreadData(inner, layoutDataList[i]);
210
267
  }
211
268
  current.appendChild(inner);
212
269
  current = inner;
@@ -215,6 +272,19 @@ export class NkRouter {
215
272
  current.appendChild(pageEl);
216
273
  return outerLayout;
217
274
  }
275
+ /** Spread loader data as individual properties on an element. */
276
+ spreadData(el, data) {
277
+ if (data && typeof data === 'object') {
278
+ const BLOCKED = new Set(['__proto__', 'constructor', 'prototype',
279
+ 'innerHTML', 'outerHTML', 'textContent',
280
+ 'render', 'connectedCallback', 'disconnectedCallback']);
281
+ for (const [key, value] of Object.entries(data)) {
282
+ if (!BLOCKED.has(key)) {
283
+ el[key] = value;
284
+ }
285
+ }
286
+ }
287
+ }
218
288
  createPageElement(route, loaderData) {
219
289
  const el = document.createElement(route.tagName);
220
290
  for (const [key, value] of Object.entries(this.params)) {
@@ -222,6 +292,7 @@ export class NkRouter {
222
292
  }
223
293
  if (loaderData !== undefined) {
224
294
  el.loaderData = loaderData;
295
+ this.spreadData(el, loaderData);
225
296
  }
226
297
  return el;
227
298
  }
@@ -230,6 +301,47 @@ export class NkRouter {
230
301
  return null;
231
302
  return this.outlet.querySelector(tagName) ?? this.outlet.querySelector(`${tagName}:last-child`);
232
303
  }
304
+ /**
305
+ * Resolve the page title from the route's meta export and update
306
+ * document.title, the aria-live announcer, and focus.
307
+ */
308
+ async updatePageMeta(route, loaderData) {
309
+ let pageTitle;
310
+ if (route.hasMeta && route.load) {
311
+ try {
312
+ const mod = await route.load();
313
+ if (mod) {
314
+ let meta;
315
+ if (typeof mod.meta === 'function') {
316
+ meta = mod.meta({ data: loaderData, params: this.params });
317
+ }
318
+ else if (mod.meta && typeof mod.meta === 'object') {
319
+ meta = mod.meta;
320
+ }
321
+ if (meta?.title) {
322
+ pageTitle = `${meta.title} | ${this.siteTitle}`;
323
+ }
324
+ }
325
+ }
326
+ catch { /* fall back to site title */ }
327
+ }
328
+ const title = pageTitle || this.siteTitle;
329
+ document.title = title;
330
+ // Announce route change to screen readers
331
+ const announcer = document.getElementById('nk-route-announcer');
332
+ if (announcer) {
333
+ announcer.textContent = '';
334
+ // Use a microtask delay so aria-live picks up the change
335
+ requestAnimationFrame(() => { announcer.textContent = title; });
336
+ }
337
+ // Move focus to the router outlet for keyboard/screen reader users
338
+ if (this.outlet) {
339
+ if (!this.outlet.hasAttribute('tabindex')) {
340
+ this.outlet.setAttribute('tabindex', '-1');
341
+ }
342
+ this.outlet.focus({ preventScroll: true });
343
+ }
344
+ }
233
345
  handleLinkClick(event) {
234
346
  const path = event.composedPath();
235
347
  const anchor = path.find((el) => el instanceof HTMLElement && el.tagName === 'A');
@@ -238,9 +350,27 @@ export class NkRouter {
238
350
  const href = anchor.getAttribute('href');
239
351
  if (!href || href.startsWith('http') || href.startsWith('#') || anchor.hasAttribute('target'))
240
352
  return;
353
+ // Allow modifier-key clicks to behave normally (Ctrl+Click = new tab, etc.)
354
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
355
+ return;
241
356
  event.preventDefault();
242
357
  this.navigate(this.stripLocale(href));
243
358
  }
359
+ async prefetch(fullPath) {
360
+ const pathname = fullPath.split('?')[0];
361
+ const match = this.matchRoute(pathname);
362
+ if (!match)
363
+ return;
364
+ const layouts = match.route.layouts || [];
365
+ await Promise.all([
366
+ // Preload component JS chunks
367
+ match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
368
+ ...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
369
+ // Prefetch loader data (cached)
370
+ match.route.hasLoader ? prefetchLoaderData(pathname, match.params).catch(() => { }) : undefined,
371
+ ...layouts.map(l => l.hasLoader ? prefetchLayoutLoaderData(l.loaderPath || '').catch(() => { }) : undefined),
372
+ ]);
373
+ }
244
374
  /** Strip locale prefix from a path for internal route matching. */
245
375
  stripLocale(path) {
246
376
  const config = getI18nConfig();
@@ -252,3 +382,19 @@ export class NkRouter {
252
382
  return config ? buildLocalePath(getLocale(), path) : path;
253
383
  }
254
384
  }
385
+ /** Navigate via the client-side router. Falls back to full reload for unknown routes. */
386
+ export function navigate(href) {
387
+ const nav = window.__nk_navigate;
388
+ if (nav) {
389
+ nav(href);
390
+ }
391
+ else {
392
+ window.location.href = href;
393
+ }
394
+ }
395
+ /** Programmatically prefetch a route's JS chunks and loader data. */
396
+ export function prefetch(href) {
397
+ const pf = window.__nk_prefetch;
398
+ if (pf)
399
+ pf(href);
400
+ }
@@ -0,0 +1,2 @@
1
+ export declare function connectSocket(routePath: string, params: Record<string, string>): Promise<any>;
2
+ export declare function disconnectAllSockets(): void;
@@ -0,0 +1,30 @@
1
+ import { getI18nConfig, getLocale } from './i18n.js';
2
+ const connections = new Map();
3
+ export async function connectSocket(routePath, params) {
4
+ const { io } = await import('socket.io-client');
5
+ const ns = `/nk${routePath === '/' ? '/index' : routePath}`;
6
+ const query = {};
7
+ if (Object.keys(params).length > 0) {
8
+ query.__params = JSON.stringify(params);
9
+ }
10
+ try {
11
+ const config = getI18nConfig();
12
+ if (config) {
13
+ query.__locale = getLocale();
14
+ }
15
+ }
16
+ catch { }
17
+ // Disconnect existing socket for this route to prevent leaks
18
+ const existing = connections.get(routePath);
19
+ if (existing)
20
+ existing.disconnect();
21
+ const socket = io(ns, { path: '/__nk_socketio/', query });
22
+ connections.set(routePath, socket);
23
+ return socket;
24
+ }
25
+ export function disconnectAllSockets() {
26
+ for (const [, socket] of connections) {
27
+ socket.disconnect();
28
+ }
29
+ connections.clear();
30
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WebRTC peer connection manager.
3
+ * Wraps RTCPeerConnection and wires to the LumenJS communication SDK signaling.
4
+ */
5
+ export type CallRole = 'caller' | 'callee';
6
+ export interface WebRTCCallbacks {
7
+ onRemoteStream: (stream: MediaStream) => void;
8
+ onLocalStream: (stream: MediaStream) => void;
9
+ onConnectionStateChange: (state: RTCPeerConnectionState) => void;
10
+ onIceCandidate: (candidate: RTCIceCandidate) => void;
11
+ onError: (error: Error) => void;
12
+ }
13
+ export declare class WebRTCManager {
14
+ private _pc;
15
+ private _localStream;
16
+ private _remoteStream;
17
+ private _callbacks;
18
+ private _pendingCandidates;
19
+ private _role;
20
+ constructor(callbacks: WebRTCCallbacks, iceServers?: RTCIceServer[]);
21
+ private _createPeerConnection;
22
+ get localStream(): MediaStream | null;
23
+ get remoteStream(): MediaStream | null;
24
+ get role(): CallRole;
25
+ get connectionState(): RTCPeerConnectionState | null;
26
+ /** Acquire local media (camera/mic) and add tracks to the peer connection */
27
+ startLocalMedia(video?: boolean, audio?: boolean): Promise<MediaStream>;
28
+ /** Create an SDP offer (caller side) */
29
+ createOffer(): Promise<string>;
30
+ /** Handle received SDP offer and create answer (callee side) */
31
+ handleOffer(sdp: string): Promise<string>;
32
+ /** Handle received SDP answer (caller side) */
33
+ handleAnswer(sdp: string): Promise<void>;
34
+ /** Add a received ICE candidate */
35
+ addIceCandidate(candidate: string, sdpMLineIndex: number | null, sdpMid: string | null): Promise<void>;
36
+ private _flushPendingCandidates;
37
+ /** Toggle audio mute */
38
+ setAudioEnabled(enabled: boolean): void;
39
+ /** Toggle video */
40
+ setVideoEnabled(enabled: boolean): void;
41
+ /** Replace video track with screen share */
42
+ startScreenShare(): Promise<MediaStream>;
43
+ /** Revert from screen share back to camera */
44
+ stopScreenShare(): Promise<void>;
45
+ /** Clean up everything */
46
+ destroy(): void;
47
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * WebRTC peer connection manager.
3
+ * Wraps RTCPeerConnection and wires to the LumenJS communication SDK signaling.
4
+ */
5
+ const ICE_SERVERS = [
6
+ { urls: 'stun:stun.l.google.com:19302' },
7
+ { urls: 'stun:stun1.l.google.com:19302' },
8
+ ];
9
+ export class WebRTCManager {
10
+ constructor(callbacks, iceServers) {
11
+ this._pc = null;
12
+ this._localStream = null;
13
+ this._remoteStream = null;
14
+ this._pendingCandidates = [];
15
+ this._role = 'caller';
16
+ this._callbacks = callbacks;
17
+ this._createPeerConnection(iceServers || ICE_SERVERS);
18
+ }
19
+ _createPeerConnection(iceServers) {
20
+ this._pc = new RTCPeerConnection({ iceServers });
21
+ this._pc.onicecandidate = (event) => {
22
+ if (event.candidate) {
23
+ this._callbacks.onIceCandidate(event.candidate);
24
+ }
25
+ };
26
+ this._pc.ontrack = (event) => {
27
+ if (!this._remoteStream) {
28
+ this._remoteStream = new MediaStream();
29
+ this._callbacks.onRemoteStream(this._remoteStream);
30
+ }
31
+ this._remoteStream.addTrack(event.track);
32
+ };
33
+ this._pc.onconnectionstatechange = () => {
34
+ if (this._pc) {
35
+ this._callbacks.onConnectionStateChange(this._pc.connectionState);
36
+ }
37
+ };
38
+ }
39
+ get localStream() { return this._localStream; }
40
+ get remoteStream() { return this._remoteStream; }
41
+ get role() { return this._role; }
42
+ get connectionState() { return this._pc?.connectionState ?? null; }
43
+ /** Acquire local media (camera/mic) and add tracks to the peer connection */
44
+ async startLocalMedia(video = true, audio = true) {
45
+ try {
46
+ this._localStream = await navigator.mediaDevices.getUserMedia({ video, audio });
47
+ this._callbacks.onLocalStream(this._localStream);
48
+ for (const track of this._localStream.getTracks()) {
49
+ this._pc?.addTrack(track, this._localStream);
50
+ }
51
+ return this._localStream;
52
+ }
53
+ catch (err) {
54
+ this._callbacks.onError(new Error(`Failed to access media: ${err.message}`));
55
+ throw err;
56
+ }
57
+ }
58
+ /** Create an SDP offer (caller side) */
59
+ async createOffer() {
60
+ this._role = 'caller';
61
+ if (!this._pc)
62
+ throw new Error('No peer connection');
63
+ const offer = await this._pc.createOffer();
64
+ await this._pc.setLocalDescription(offer);
65
+ return offer.sdp;
66
+ }
67
+ /** Handle received SDP offer and create answer (callee side) */
68
+ async handleOffer(sdp) {
69
+ this._role = 'callee';
70
+ if (!this._pc)
71
+ throw new Error('No peer connection');
72
+ await this._pc.setRemoteDescription({ type: 'offer', sdp });
73
+ // Flush pending ICE candidates
74
+ await this._flushPendingCandidates();
75
+ const answer = await this._pc.createAnswer();
76
+ await this._pc.setLocalDescription(answer);
77
+ return answer.sdp;
78
+ }
79
+ /** Handle received SDP answer (caller side) */
80
+ async handleAnswer(sdp) {
81
+ if (!this._pc)
82
+ throw new Error('No peer connection');
83
+ await this._pc.setRemoteDescription({ type: 'answer', sdp });
84
+ await this._flushPendingCandidates();
85
+ }
86
+ /** Add a received ICE candidate */
87
+ async addIceCandidate(candidate, sdpMLineIndex, sdpMid) {
88
+ const init = {
89
+ candidate,
90
+ sdpMLineIndex: sdpMLineIndex ?? undefined,
91
+ sdpMid: sdpMid ?? undefined,
92
+ };
93
+ if (!this._pc?.remoteDescription) {
94
+ // Queue candidates until remote description is set
95
+ this._pendingCandidates.push(init);
96
+ return;
97
+ }
98
+ try {
99
+ await this._pc.addIceCandidate(init);
100
+ }
101
+ catch (err) {
102
+ console.warn('[WebRTC] Failed to add ICE candidate:', err);
103
+ }
104
+ }
105
+ async _flushPendingCandidates() {
106
+ for (const c of this._pendingCandidates) {
107
+ try {
108
+ await this._pc?.addIceCandidate(c);
109
+ }
110
+ catch { }
111
+ }
112
+ this._pendingCandidates = [];
113
+ }
114
+ /** Toggle audio mute */
115
+ setAudioEnabled(enabled) {
116
+ if (this._localStream) {
117
+ for (const track of this._localStream.getAudioTracks()) {
118
+ track.enabled = enabled;
119
+ }
120
+ }
121
+ }
122
+ /** Toggle video */
123
+ setVideoEnabled(enabled) {
124
+ if (this._localStream) {
125
+ for (const track of this._localStream.getVideoTracks()) {
126
+ track.enabled = enabled;
127
+ }
128
+ }
129
+ }
130
+ /** Replace video track with screen share */
131
+ async startScreenShare() {
132
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
133
+ const screenTrack = stream.getVideoTracks()[0];
134
+ if (this._pc && this._localStream) {
135
+ const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
136
+ if (sender) {
137
+ await sender.replaceTrack(screenTrack);
138
+ }
139
+ }
140
+ // When user stops sharing via browser UI
141
+ screenTrack.onended = () => {
142
+ this.stopScreenShare();
143
+ };
144
+ return stream;
145
+ }
146
+ /** Revert from screen share back to camera */
147
+ async stopScreenShare() {
148
+ if (this._localStream && this._pc) {
149
+ const cameraTrack = this._localStream.getVideoTracks()[0];
150
+ if (cameraTrack) {
151
+ const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
152
+ if (sender) {
153
+ await sender.replaceTrack(cameraTrack);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ /** Clean up everything */
159
+ destroy() {
160
+ if (this._localStream) {
161
+ for (const track of this._localStream.getTracks()) {
162
+ track.stop();
163
+ }
164
+ this._localStream = null;
165
+ }
166
+ if (this._remoteStream) {
167
+ for (const track of this._remoteStream.getTracks()) {
168
+ track.stop();
169
+ }
170
+ this._remoteStream = null;
171
+ }
172
+ if (this._pc) {
173
+ this._pc.close();
174
+ this._pc = null;
175
+ }
176
+ this._pendingCandidates = [];
177
+ }
178
+ }
@@ -0,0 +1,8 @@
1
+ import type { Server } from 'http';
2
+ export interface ShutdownConfig {
3
+ /** Max time to wait for connections to drain (ms). Default: 30000. */
4
+ timeout?: number;
5
+ /** Extra cleanup functions to run before exit. */
6
+ onShutdown?: () => Promise<void> | void;
7
+ }
8
+ export declare function setupGracefulShutdown(server: Server, config?: ShutdownConfig): void;
@@ -0,0 +1,36 @@
1
+ import { logger } from './logger.js';
2
+ export function setupGracefulShutdown(server, config) {
3
+ const timeout = config?.timeout ?? 30_000;
4
+ let isShuttingDown = false;
5
+ const shutdown = async (signal) => {
6
+ if (isShuttingDown)
7
+ return;
8
+ isShuttingDown = true;
9
+ logger.info(`Received ${signal}, starting graceful shutdown...`);
10
+ // Stop accepting new connections
11
+ server.close(() => {
12
+ logger.info('All connections drained.');
13
+ });
14
+ // Force-close after timeout
15
+ const forceTimer = setTimeout(() => {
16
+ logger.warn('Shutdown timeout reached, forcing exit.', { timeout });
17
+ process.exit(1);
18
+ }, timeout);
19
+ forceTimer.unref();
20
+ // Run custom cleanup
21
+ if (config?.onShutdown) {
22
+ try {
23
+ await config.onShutdown();
24
+ logger.info('Custom cleanup completed.');
25
+ }
26
+ catch (err) {
27
+ logger.error('Error during custom cleanup.', { error: err?.message });
28
+ }
29
+ }
30
+ // Close idle keep-alive connections
31
+ server.closeIdleConnections();
32
+ logger.info('Graceful shutdown complete.');
33
+ };
34
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
35
+ process.on('SIGINT', () => shutdown('SIGINT'));
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http';
2
+ export interface HealthCheckConfig {
3
+ /** Endpoint path. Default: '/__health'. */
4
+ path?: string;
5
+ /** App version string. Default: reads from package.json or 'unknown'. */
6
+ version?: string;
7
+ }
8
+ export declare function createHealthCheckHandler(config?: HealthCheckConfig): (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => void;
@@ -0,0 +1,25 @@
1
+ const startTime = Date.now();
2
+ export function createHealthCheckHandler(config) {
3
+ const healthPath = config?.path || '/__health';
4
+ const version = config?.version || process.env.npm_package_version || 'unknown';
5
+ return (req, res, next) => {
6
+ if (req.url?.split('?')[0] !== healthPath)
7
+ return next();
8
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
9
+ const body = JSON.stringify({
10
+ status: 'ok',
11
+ uptime,
12
+ version,
13
+ timestamp: new Date().toISOString(),
14
+ memory: {
15
+ rss: Math.round(process.memoryUsage().rss / 1024 / 1024),
16
+ heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
17
+ },
18
+ });
19
+ res.writeHead(200, {
20
+ 'Content-Type': 'application/json',
21
+ 'Cache-Control': 'no-store',
22
+ });
23
+ res.end(body);
24
+ };
25
+ }