@abtnode/core 1.17.7-beta-20251227-001958-ea2ba3f5 → 1.17.7-beta-20251229-223813-e1e6c5e3

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 (39) hide show
  1. package/lib/blocklet/manager/disk.js +74 -32
  2. package/lib/blocklet/manager/ensure-blocklet-running.js +1 -1
  3. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +1 -1
  4. package/lib/blocklet/manager/helper/install-application-from-general.js +2 -3
  5. package/lib/blocklet/manager/helper/install-component-from-url.js +7 -4
  6. package/lib/blocklet/migration-dist/migration.cjs +5 -4
  7. package/lib/blocklet/passport/index.js +10 -3
  8. package/lib/blocklet/project/index.js +7 -2
  9. package/lib/blocklet/security/index.js +2 -2
  10. package/lib/cert.js +6 -3
  11. package/lib/event/index.js +98 -87
  12. package/lib/event/util.js +7 -13
  13. package/lib/index.js +15 -26
  14. package/lib/migrations/1.5.0-site.js +3 -7
  15. package/lib/migrations/1.5.15-site.js +3 -7
  16. package/lib/monitor/blocklet-runtime-monitor.js +37 -5
  17. package/lib/monitor/node-runtime-monitor.js +4 -4
  18. package/lib/router/helper.js +525 -452
  19. package/lib/router/index.js +280 -104
  20. package/lib/router/manager.js +14 -28
  21. package/lib/states/blocklet-child.js +93 -1
  22. package/lib/states/blocklet-extras.js +1 -1
  23. package/lib/states/blocklet.js +429 -197
  24. package/lib/states/node.js +0 -10
  25. package/lib/states/site.js +87 -4
  26. package/lib/team/manager.js +2 -21
  27. package/lib/util/blocklet.js +39 -19
  28. package/lib/util/get-accessible-external-node-ip.js +21 -6
  29. package/lib/util/index.js +3 -3
  30. package/lib/util/ip.js +15 -1
  31. package/lib/util/launcher.js +11 -11
  32. package/lib/util/ready.js +2 -9
  33. package/lib/util/reset-node.js +6 -5
  34. package/lib/validators/router.js +0 -3
  35. package/lib/webhook/sender/api/index.js +5 -0
  36. package/package.json +23 -25
  37. package/lib/migrations/1.0.36-snapshot.js +0 -10
  38. package/lib/migrations/1.1.9-snapshot.js +0 -7
  39. package/lib/states/routing-snapshot.js +0 -146
@@ -1,11 +1,12 @@
1
+ /* eslint-disable no-use-before-define */
1
2
  /* eslint-disable no-restricted-syntax */
2
3
  /* eslint-disable prefer-destructuring */
3
4
  const fs = require('fs-extra');
4
5
  const path = require('path');
5
6
  const tar = require('tar');
7
+ const dns = require('dns').promises;
6
8
  const UUID = require('uuid');
7
9
  const dayjs = require('@abtnode/util/lib/dayjs');
8
- const isUrl = require('is-url');
9
10
  const get = require('lodash/get');
10
11
  const cloneDeep = require('@abtnode/util/lib/deep-clone');
11
12
  const groupBy = require('lodash/groupBy');
@@ -26,7 +27,8 @@ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
26
27
  const getTmpDir = require('@abtnode/util/lib/get-tmp-directory');
27
28
  const downloadFile = require('@abtnode/util/lib/download-file');
28
29
  const axios = require('@abtnode/util/lib/axios');
29
- const { getIpDnsDomainForBlocklet } = require('@abtnode/util/lib/get-domain-for-blocklet');
30
+ const { getIpDnsDomainForBlocklet, getDidDomainForBlocklet } = require('@abtnode/util/lib/get-domain-for-blocklet');
31
+ const { hasMountPoint } = require('@blocklet/meta/lib/engine');
30
32
  const { forEachBlockletSync } = require('@blocklet/meta/lib/util');
31
33
  const { processLogByDate } = require('@abtnode/analytics');
32
34
  const { DBCache, getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
@@ -43,7 +45,6 @@ const {
43
45
  NAME_FOR_WELLKNOWN_SITE,
44
46
  ROUTING_RULE_TYPES,
45
47
  CERTIFICATE_EXPIRES_OFFSET,
46
- DEFAULT_SERVICE_PATH,
47
48
  SLOT_FOR_IP_DNS_SITE,
48
49
  BLOCKLET_SITE_GROUP_SUFFIX,
49
50
  WELLKNOWN_ACME_CHALLENGE_PREFIX,
@@ -57,6 +58,8 @@ const {
57
58
  DEFAULT_IP_DOMAIN,
58
59
  WELLKNOWN_BLACKLIST_PREFIX,
59
60
  NODE_MODES,
61
+ ACCESS_POLICY_PUBLIC,
62
+ DEFAULT_DID_DOMAIN,
60
63
  } = require('@abtnode/constant');
61
64
  const {
62
65
  BLOCKLET_DYNAMIC_PATH_PREFIX,
@@ -64,17 +67,15 @@ const {
64
67
  BLOCKLET_INTERFACE_PUBLIC,
65
68
  BLOCKLET_INTERFACE_WELLKNOWN,
66
69
  BLOCKLET_INTERFACE_TYPE_WELLKNOWN,
67
- BLOCKLET_CONFIGURABLE_KEY,
68
70
  BlockletEvents,
69
71
  BLOCKLET_MODES,
70
72
  } = require('@blocklet/constant');
71
- const { isInstanceWorker } = require('@abtnode/util/lib/pm2/is-instance-worker');
73
+ const { isWorkerInstance } = require('@abtnode/util/lib/pm2/is-instance-worker');
72
74
 
73
75
  const pkg = require('../../package.json');
74
76
  // eslint-disable-next-line
75
77
  const logger = require('@abtnode/logger')(`${pkg.name}:router:helper`);
76
78
  const {
77
- getProviderFromNodeInfo,
78
79
  getHttpsCertInfo,
79
80
  findInterfacePortByName,
80
81
  getWellknownSitePort,
@@ -103,6 +104,7 @@ const {
103
104
  } = require('../util/blocklet');
104
105
  const { toCamelCase } = require('../util/index');
105
106
  const { get: getIp } = require('../util/ip');
107
+ const { getBlockletSecurityRules } = require('../blocklet/security/security-rule');
106
108
 
107
109
  const isServiceFeDevelopment = process.env.ABT_NODE_SERVICE_FE_PORT;
108
110
 
@@ -217,57 +219,9 @@ const attachRuntimeDomainAliases = async ({ sites = [], context = {} }) => {
217
219
  });
218
220
  };
219
221
 
220
- /**
221
- * @description
222
- * @param {{
223
- * corsAllowedOrigins: Array<string>;
224
- * }} site
225
- * @param {string} rawUrl
226
- * @return {void}
227
- */
228
- const addCorsToSite = (site, rawUrl) => {
229
- if (!site || !rawUrl) {
230
- return;
231
- }
232
- try {
233
- const url = new URL(rawUrl);
234
- if (!Array.isArray(site.corsAllowedOrigins)) {
235
- site.corsAllowedOrigins = [];
236
- }
237
-
238
- if (!site.corsAllowedOrigins.includes(url.hostname)) {
239
- site.corsAllowedOrigins.push(url.hostname);
240
- }
241
- } catch (err) {
242
- console.error('addCorsToSite', err);
243
- }
244
- };
245
-
246
222
  const isBasicSite = (domain) =>
247
223
  [DOMAIN_FOR_INTERNAL_SITE, DOMAIN_FOR_IP_SITE, DOMAIN_FOR_DEFAULT_SITE, DOMAIN_FOR_IP_SITE_REGEXP].includes(domain);
248
224
 
249
- /**
250
- * add rule of directly proxy to abtnode-service for default site
251
- */
252
- const ensureServiceRule = async (sites) => {
253
- const info = await states.node.read();
254
- return sites.map((site) => {
255
- if (
256
- [DOMAIN_FOR_IP_SITE, DOMAIN_FOR_IP_SITE_REGEXP].includes(site.domain) &&
257
- !site.rules.some((r) => r.from.pathPrefix === DEFAULT_SERVICE_PATH)
258
- ) {
259
- site.rules.push({
260
- from: { pathPrefix: DEFAULT_SERVICE_PATH },
261
- to: {
262
- port: process.env.ABT_NODE_SERVICE_PORT,
263
- type: ROUTING_RULE_TYPES.SERVICE,
264
- did: info.did,
265
- },
266
- });
267
- }
268
- return site;
269
- });
270
- };
271
225
  const ensureRootRule = (sites) => {
272
226
  return sites.map((site) => {
273
227
  if (!isBasicSite(site.domain) && !site.rules.some((x) => x.from.pathPrefix === '/')) {
@@ -282,8 +236,7 @@ const ensureRootRule = (sites) => {
282
236
  });
283
237
  };
284
238
 
285
- const ensureLatestNodeInfo = async (sites = [], { withDefaultCors = true } = {}) => {
286
- const info = await states.node.read();
239
+ const ensureLatestNodeInfo = (sites = [], info) => {
287
240
  return sites.map((site) => {
288
241
  site.rules = site.rules.map((rule) => {
289
242
  if (rule.to.did === info.did && rule.to.type === ROUTING_RULE_TYPES.DAEMON) {
@@ -300,14 +253,6 @@ const ensureLatestNodeInfo = async (sites = [], { withDefaultCors = true } = {})
300
253
  // We use an regular expression to match ip domain so that it is adaptive
301
254
  // @ref https://stackoverflow.com/questions/9454764/nginx-server-name-wildcard-or-catch-all
302
255
  site.domain = DOMAIN_FOR_IP_SITE_REGEXP;
303
-
304
- if (withDefaultCors) {
305
- // Allow CORS from "Install on ABT Node"
306
- addCorsToSite(site, info.registerUrl);
307
-
308
- // Allow CORS from "Web Wallet"
309
- addCorsToSite(site, info.webWalletUrl);
310
- }
311
256
  }
312
257
 
313
258
  return site;
@@ -317,9 +262,8 @@ const ensureLatestNodeInfo = async (sites = [], { withDefaultCors = true } = {})
317
262
  /**
318
263
  * set rule.to.target by interface.path and interface.prefix
319
264
  */
320
- const ensureLatestInterfaceInfo = async (sites = []) => {
321
- // FIXME @linchen groupAllInterfaces() may have performance issue
322
- const interfaces = await states.blocklet.groupAllInterfaces();
265
+ const ensureLatestInterfaceInfo = async (sites = [], blocklets = []) => {
266
+ const interfaces = await states.blocklet.groupAllInterfaces(blocklets);
323
267
  return sites.map((site) => {
324
268
  if (!Array.isArray(site.rules)) {
325
269
  return site;
@@ -370,6 +314,7 @@ const ensureWellknownRule = async (sites = []) => {
370
314
  port: wellknownPort,
371
315
  type: ROUTING_RULE_TYPES.GENERAL_PROXY,
372
316
  interfaceName: BLOCKLET_INTERFACE_WELLKNOWN,
317
+ did: site.blockletDid,
373
318
  },
374
319
  isProtected: true,
375
320
  });
@@ -391,7 +336,6 @@ const ensureWellknownRule = async (sites = []) => {
391
336
  grouped['/'] = [
392
337
  {
393
338
  id: '',
394
- groupId: '',
395
339
  to: {
396
340
  did: site.blockletDid,
397
341
  componentId: site.blockletDid,
@@ -409,7 +353,6 @@ const ensureWellknownRule = async (sites = []) => {
409
353
  if (!site.rules.some((x) => x.from.pathPrefix === servicePathPrefix)) {
410
354
  site.rules.push({
411
355
  id: rule.id,
412
- groupId: rule.groupId,
413
356
  from: {
414
357
  pathPrefix: servicePathPrefix,
415
358
  groupPathPrefix,
@@ -430,7 +373,6 @@ const ensureWellknownRule = async (sites = []) => {
430
373
  if (!site.rules.some((x) => x.from.pathPrefix === avatarPathPrefix)) {
431
374
  site.rules.push({
432
375
  id: rule.id,
433
- groupId: rule.groupId,
434
376
  from: {
435
377
  pathPrefix: avatarPathPrefix,
436
378
  groupPathPrefix,
@@ -453,10 +395,8 @@ const ensureWellknownRule = async (sites = []) => {
453
395
  return sites;
454
396
  };
455
397
 
456
- const ensureBlockletDid = async (sites) => {
457
- const info = await states.node.read();
458
-
459
- return (sites || []).map((site) => {
398
+ const ensureBlockletDid = (sites = [], info) => {
399
+ return sites.map((site) => {
460
400
  if (site.domain === DOMAIN_FOR_INTERNAL_SITE) {
461
401
  return site;
462
402
  }
@@ -472,39 +412,6 @@ const ensureBlockletDid = async (sites) => {
472
412
  });
473
413
  };
474
414
 
475
- const ensureCorsForWebWallet = async (sites) => {
476
- const info = await states.node.read();
477
- for (const site of sites) {
478
- if (!isBasicSite(site.domain)) {
479
- // Allow CORS from "Web Wallet"
480
- addCorsToSite(site, info.webWalletUrl);
481
- }
482
- }
483
- return sites;
484
- };
485
-
486
- /**
487
- * @description
488
- * @param {Array<any>} [sites=[]]
489
- * @param {Array<import('@blocklet/server-js').BlockletState>} blocklets
490
- * @return {Promise<any>}
491
- */
492
- const ensureCorsForDidSpace = (sites = [], blocklets) => {
493
- return sites.map((site) => {
494
- const blocklet = blocklets.find((x) => x.meta.did === site.blockletDid);
495
- if (blocklet) {
496
- const endpoint = blocklet.environments.find(
497
- (x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT
498
- );
499
- if (endpoint && isUrl(endpoint.value)) {
500
- addCorsToSite(site, endpoint.value);
501
- }
502
- }
503
-
504
- return site;
505
- });
506
- };
507
-
508
415
  const filterSitesForRemovedBlocklets = (sites = [], blocklets) => {
509
416
  return sites.filter((site) => {
510
417
  if (!site.domain.endsWith(BLOCKLET_SITE_GROUP_SUFFIX)) {
@@ -589,8 +496,6 @@ const ensureBlockletWellknownRules = (sites, blocklets) => {
589
496
  .filter(Boolean);
590
497
  };
591
498
 
592
- // Expand component rules to blocklet rules
593
- const isComponentRule = (x) => x.to.type === ROUTING_RULE_TYPES.COMPONENT;
594
499
  const expandComponentRules = (sites = [], blocklets) => {
595
500
  return sites
596
501
  .map((site) => {
@@ -603,50 +508,43 @@ const expandComponentRules = (sites = [], blocklets) => {
603
508
  }
604
509
 
605
510
  const blocklet = blocklets.find((x) => x.meta.did === site.blockletDid);
606
- const components = blocklet.children.filter((x) => x.mountPoint !== '/' && x.mode === BLOCKLET_MODES.PRODUCTION);
607
- const expandedRules = [];
608
-
609
- site.rules.filter(isComponentRule).forEach((baseRule) => {
610
- components.forEach((x) => {
611
- if (!hasStartEngine(x.meta)) {
612
- return;
613
- }
614
- const newRule = {
615
- id: UUID.v4(),
616
- groupId: baseRule.groupId,
617
- from: {
618
- groupPathPrefix: '/',
619
- },
620
- to: {
621
- type: ROUTING_RULE_TYPES.BLOCKLET,
622
- componentId: getComponentId(x, [blocklet]),
623
- interfaceName: BLOCKLET_INTERFACE_PUBLIC,
624
- port: findInterfacePortByName(x, BLOCKLET_INTERFACE_PUBLIC),
625
- did: blocklet.meta.did,
626
- target: '/',
627
- },
628
- };
629
-
630
- if (x.meta.did === baseRule.to.componentId) {
631
- newRule.from.pathPrefix = baseRule.from.pathPrefix;
632
- newRule.to.pageGroup = baseRule.to.pageGroup;
633
- } else {
634
- newRule.from.pathPrefix = joinURL(baseRule.from.pathPrefix, x.mountPoint);
635
- }
636
-
637
- expandedRules.push(newRule);
638
- });
511
+ const components = blocklet.children.filter((x) => x.mode === BLOCKLET_MODES.PRODUCTION);
512
+ const expandedRules = components
513
+ .filter((x) => hasMountPoint(x.meta))
514
+ .map((x) => ({
515
+ id: UUID.v4(),
516
+ from: {
517
+ pathPrefix: x.mountPoint,
518
+ groupPathPrefix: '/',
519
+ },
520
+ to: {
521
+ type: ROUTING_RULE_TYPES.BLOCKLET,
522
+ componentId: getComponentId(x, [blocklet]),
523
+ interfaceName: BLOCKLET_INTERFACE_PUBLIC,
524
+ port: findInterfacePortByName(x, BLOCKLET_INTERFACE_PUBLIC),
525
+ did: blocklet.meta.did,
526
+ target: '/',
527
+ pageGroup: '',
528
+ },
529
+ }));
530
+
531
+ // logger.info('expandComponentRules.before.rules', { siteId: site.id, rules: site.rules, expandedRules });
532
+ site.rules = site.rules.filter((x) => x.isProtected).concat(expandedRules);
533
+ site.rules.forEach((x) => {
534
+ if (x.to.type === ROUTING_RULE_TYPES.COMPONENT) {
535
+ x.to.type = ROUTING_RULE_TYPES.BLOCKLET;
536
+ x.to.componentId = [blocklet.meta.did, x.to.componentId].join('/');
537
+ }
639
538
  });
539
+ // logger.info('expandComponentRules.after.rules', { siteId: site.id, rules: site.rules });
640
540
 
641
- site.rules = site.rules.filter((x) => !isComponentRule(x)).concat(expandedRules);
642
541
  site.componentExpanded = true;
643
-
644
542
  return site;
645
543
  })
646
544
  .filter(Boolean);
647
545
  };
648
546
 
649
- const ensureBlockletCache = (sites = [], blocklets) => {
547
+ const ensureBlockletCache = (sites = [], blocklets = []) => {
650
548
  return sites
651
549
  .map((site) => {
652
550
  if (!site.domain.endsWith(BLOCKLET_SITE_GROUP_SUFFIX)) {
@@ -696,7 +594,7 @@ const ensureBlockletCache = (sites = [], blocklets) => {
696
594
  .filter(Boolean);
697
595
  };
698
596
 
699
- const ensureBlockletProxyBehavior = (sites, blocklets) => {
597
+ const ensureBlockletProxyBehavior = (sites, blocklets = []) => {
700
598
  return sites
701
599
  .map((site) => {
702
600
  if (!site.domain.endsWith(BLOCKLET_SITE_GROUP_SUFFIX)) {
@@ -727,21 +625,130 @@ const ensureBlockletProxyBehavior = (sites, blocklets) => {
727
625
  .filter(Boolean);
728
626
  };
729
627
 
730
- const ensureLatestInfo = async (sites = [], { withDefaultCors = true } = {}) => {
731
- const blocklets = await states.blocklet.getBlocklets();
628
+ /**
629
+ * Get static root path for a component
630
+ * @param {object} component - Blocklet component
631
+ * @returns {string|null} - Absolute path to static files root, or null if not available
632
+ */
633
+ const getStaticRoot = (component) => {
634
+ const appDir = component.environments.find((e) => e.key === 'BLOCKLET_APP_DIR')?.value;
635
+ if (!appDir) {
636
+ return null;
637
+ }
638
+
639
+ const main = component.meta?.main;
640
+ return main ? path.join(appDir, main) : appDir;
641
+ };
642
+
643
+ /**
644
+ * Check if a component can be served directly by Nginx
645
+ * Conditions:
646
+ * 1. Engine-based blocklet using 'blocklet' interpreter (static-server engine)
647
+ * 2. Engine source is the built-in static-server (not a custom engine)
648
+ * 3. Has a default security rule (pathPattern: '*') with ACCESS_POLICY_PUBLIC
649
+ *
650
+ * @param {object} component - Blocklet component
651
+ * @param {Array} securityRules - Security rules for the blocklet
652
+ * @returns {boolean} - True if can serve static directly
653
+ */
654
+ const canServeStaticDirectly = (component, securityRules) => {
655
+ if (hasStartEngine(component.meta)) {
656
+ return false;
657
+ }
658
+
659
+ if (!securityRules || securityRules.length === 0) {
660
+ return false;
661
+ }
662
+
663
+ const componentRules = securityRules.filter((x) => x.componentDid === component.meta.did);
664
+ if (componentRules.length) {
665
+ return componentRules.every((x) => x.accessPolicy.id === ACCESS_POLICY_PUBLIC);
666
+ }
667
+
668
+ const fallbackRules = securityRules.filter((x) => !x.componentDid);
669
+ if (fallbackRules.length) {
670
+ return fallbackRules.every((x) => x.accessPolicy.id === ACCESS_POLICY_PUBLIC);
671
+ }
672
+
673
+ return false;
674
+ };
675
+
676
+ /**
677
+ * Ensure static serving info is added to routing rules for eligible blocklets
678
+ * This function adds serveStatic and staticRoot fields to rules that:
679
+ * 1. Are engine-based static blocklets
680
+ * 2. Have public access policy for all paths
681
+ *
682
+ * @param {Array} sites - Routing sites
683
+ * @param {Array} blocklets - All blocklets
684
+ * @param {object} teamManager - Team manager for querying security rules
685
+ * @returns {Promise<Array>} - Sites with static serving info added
686
+ */
687
+ const ensureBlockletStaticServing = async (sites = [], blocklets = [], teamManager = null) => {
688
+ if (!teamManager) {
689
+ return sites;
690
+ }
691
+
692
+ const result = await Promise.all(
693
+ sites.map(async (site) => {
694
+ if (!site.domain.endsWith(BLOCKLET_SITE_GROUP_SUFFIX)) {
695
+ return site;
696
+ }
697
+
698
+ const blocklet = blocklets.find((x) => x.meta.did === site.blockletDid);
699
+ if (!blocklet) {
700
+ return site;
701
+ }
702
+
703
+ // Get security rules for this blocklet
704
+ const { securityRules } = await getBlockletSecurityRules(
705
+ { teamManager },
706
+ { did: blocklet.meta.did, formatted: true }
707
+ );
708
+
709
+ // Process each rule
710
+ site.rules
711
+ .filter((x) => x.to.type === ROUTING_RULE_TYPES.BLOCKLET && x.to.interfaceName === BLOCKLET_INTERFACE_PUBLIC)
712
+ .forEach((rule) => {
713
+ const component = findComponentById(blocklet, rule.to.componentId);
714
+ if (!component) {
715
+ return;
716
+ }
717
+
718
+ // Check if this component can be served directly
719
+ if (canServeStaticDirectly(component, securityRules)) {
720
+ const staticRoot = getStaticRoot(component);
721
+ logger.info('ensureBlockletStaticServing.canServeStaticDirectly', {
722
+ blockletDid: blocklet.meta.did,
723
+ componentDid: component.meta.did,
724
+ staticRoot,
725
+ });
726
+ if (staticRoot) {
727
+ rule.to.staticRoot = staticRoot;
728
+ }
729
+ }
730
+ });
731
+
732
+ return site;
733
+ })
734
+ );
735
+
736
+ return result.filter(Boolean);
737
+ };
732
738
 
739
+ const ensureLatestInfo = async (sites = [], blocklets = [], teamManager = null, { nodeInfo = null } = {}) => {
740
+ const info = nodeInfo ?? (await states.node.read());
733
741
  // CAUTION: following steps are very important, please do not change the order
734
- let result = await ensureLatestNodeInfo(sites, { withDefaultCors });
735
- result = await ensureBlockletDid(result);
736
- result = await filterSitesForRemovedBlocklets(sites, blocklets);
737
- result = await ensureBlockletProxyBehavior(result, blocklets);
738
- result = await ensureBlockletCache(result, blocklets);
742
+ let result = ensureLatestNodeInfo(sites, info);
743
+ result = ensureBlockletDid(result, info);
744
+ result = filterSitesForRemovedBlocklets(result, blocklets);
745
+ result = ensureBlockletProxyBehavior(result, blocklets);
746
+ result = ensureBlockletCache(result, blocklets);
739
747
  result = await ensureWellknownRule(result);
740
- result = await ensureCorsForWebWallet(result);
741
- result = await ensureCorsForDidSpace(result, blocklets);
742
- result = await ensureLatestInterfaceInfo(result);
743
- result = await ensureBlockletWellknownRules(result, blocklets);
744
- result = await expandComponentRules(result, blocklets);
748
+ result = await ensureLatestInterfaceInfo(result, blocklets);
749
+ result = ensureBlockletWellknownRules(result, blocklets);
750
+ result = expandComponentRules(result, blocklets);
751
+ result = await ensureBlockletStaticServing(result, blocklets, teamManager);
745
752
 
746
753
  return result;
747
754
  };
@@ -763,15 +770,17 @@ const getDownloadCertBaseUrl = (info) =>
763
770
  /**
764
771
  * 根据 DID 获取域名
765
772
  */
766
- const getDomainsByDid = async (did) => {
773
+ const getDomainsByDid = async (did, teamManager) => {
767
774
  if (!did) {
768
775
  return [];
769
776
  }
770
777
 
771
778
  try {
772
779
  const sites = await states.site.getSitesByBlocklet(did);
780
+ const blocklet = await states.blocklet.getBlocklet(did);
781
+ const blocklets = blocklet ? [blocklet] : [];
773
782
  const domainAliases = await attachRuntimeDomainAliases({
774
- sites: await ensureLatestInfo(sites, { withDefaultCors: false }),
783
+ sites: await ensureLatestInfo(sites, blocklets, teamManager),
775
784
  context: {},
776
785
  });
777
786
 
@@ -786,7 +795,6 @@ const getDomainsByDid = async (did) => {
786
795
 
787
796
  module.exports = function getRouterHelpers({
788
797
  dataDirs,
789
- routingSnapshot,
790
798
  routerManager,
791
799
  blockletManager,
792
800
  certManager,
@@ -1049,6 +1057,7 @@ module.exports = function getRouterHelpers({
1049
1057
  const newSiteRule = {
1050
1058
  id: site.id,
1051
1059
  rule,
1060
+ skipValidation: true, // Skip nginx validation for system rules - validated at end
1052
1061
  };
1053
1062
 
1054
1063
  const existingRule = findExistingRule(get(rule, 'from.pathPrefix'));
@@ -1067,9 +1076,13 @@ module.exports = function getRouterHelpers({
1067
1076
  return false;
1068
1077
  };
1069
1078
 
1070
- const addWellknownSite = async (sites, context) => {
1071
- const site = (sites || []).find((x) => x.name === NAME_FOR_WELLKNOWN_SITE);
1072
-
1079
+ /**
1080
+ * Add wellknown site with routing rules
1081
+ * @param {object|null} site - Pre-fetched wellknown site or null if not exists
1082
+ * @param {object} context
1083
+ * @returns {Promise<boolean>} - true if routing changed
1084
+ */
1085
+ const addWellknownSite = async (site, context) => {
1073
1086
  try {
1074
1087
  const info = await nodeState.read();
1075
1088
  const proxyTarget = {
@@ -1161,12 +1174,19 @@ module.exports = function getRouterHelpers({
1161
1174
  * Add system routing sites for the dashboard
1162
1175
  * Which should contain: default site, ip site, wellknown site
1163
1176
  *
1177
+ * Optimized to use O(1) targeted queries instead of loading all sites.
1178
+ * With thousands of blocklets, this avoids loading all sites into memory.
1179
+ *
1164
1180
  * @returns {boolean} if routing changed
1165
1181
  */
1166
1182
  const ensureDashboardRouting = async (context = {}) => {
1167
- const info = await nodeState.read();
1168
- const sites = await siteState.getSites();
1169
- let dashboardSite = (sites || []).find((x) => x.domain === DOMAIN_FOR_IP_SITE);
1183
+ // eslint-disable-next-line
1184
+ let [info, dashboardSite, defaultSite, wellknownSite] = await Promise.all([
1185
+ nodeState.read(),
1186
+ siteState.findOne({ domain: DOMAIN_FOR_IP_SITE }),
1187
+ siteState.findOne({ domain: DOMAIN_FOR_DEFAULT_SITE }),
1188
+ siteState.findOne({ name: NAME_FOR_WELLKNOWN_SITE }),
1189
+ ]);
1170
1190
  const updatedResult = [];
1171
1191
  if (!dashboardSite) {
1172
1192
  try {
@@ -1183,6 +1203,7 @@ module.exports = function getRouterHelpers({
1183
1203
  ],
1184
1204
  },
1185
1205
  skipCheckDynamicBlacklist: true,
1206
+ skipValidation: true, // Skip nginx validation for system sites
1186
1207
  },
1187
1208
  context
1188
1209
  );
@@ -1221,8 +1242,7 @@ module.exports = function getRouterHelpers({
1221
1242
  logger.error('add dashboard analytics rule failed', { error });
1222
1243
  }
1223
1244
 
1224
- const defaultRule = sites.find((x) => x.domain === DOMAIN_FOR_DEFAULT_SITE);
1225
- if (!defaultRule) {
1245
+ if (!defaultSite) {
1226
1246
  try {
1227
1247
  const result = await routerManager.addRoutingSite(
1228
1248
  {
@@ -1231,6 +1251,7 @@ module.exports = function getRouterHelpers({
1231
1251
  rules: [],
1232
1252
  },
1233
1253
  skipCheckDynamicBlacklist: true,
1254
+ skipValidation: true, // Skip nginx validation for system sites
1234
1255
  },
1235
1256
  context
1236
1257
  );
@@ -1241,28 +1262,17 @@ module.exports = function getRouterHelpers({
1241
1262
  }
1242
1263
  }
1243
1264
 
1244
- const wellknownRes = await addWellknownSite(sites, context);
1265
+ const wellknownRes = await addWellknownSite(wellknownSite, context);
1245
1266
  if (wellknownRes) {
1246
1267
  updatedResult.push(wellknownRes);
1247
1268
  }
1248
1269
 
1249
- if (updatedResult.length) {
1250
- // eslint-disable-next-line no-use-before-define
1251
- const hash = await takeRoutingSnapshot({ message: 'ensure dashboard routing rules', dryRun: false }, context);
1252
- logger.info('take routing snapshot on ensure dashboard routing rules', { updatedResult, hash });
1253
- return true;
1254
- }
1255
-
1256
- return false;
1270
+ return updatedResult.length > 0;
1257
1271
  };
1258
1272
 
1259
- async function updateSiteDomainAliases(existSite, blocklet, nodeInfo) {
1273
+ async function updateSiteDomainAliases(site, blocklet, nodeInfo) {
1260
1274
  try {
1261
- /* 在有些 场景下, site 已经存在, 需要更新 DID Domain, 比如:
1262
- * - 恢复 Blocklet
1263
- */
1264
- // 因为更新路由是非常重要的操作,所以为了确保更新路由的稳定性,在更新域名时使用 try catch 来捕获异常
1265
- const domainAliases = cloneDeep(existSite.domainAliases || []);
1275
+ const domainAliases = cloneDeep(site.domainAliases || []);
1266
1276
  const didDomainList = getBlockletDidDomainList(blocklet, nodeInfo);
1267
1277
  didDomainList.forEach((item) => {
1268
1278
  if (!domainAliases.some((alias) => alias.value === item.value)) {
@@ -1270,27 +1280,28 @@ module.exports = function getRouterHelpers({
1270
1280
  }
1271
1281
  });
1272
1282
 
1273
- if (isEqual(existSite.domainAliases, domainAliases)) {
1274
- logger.info('existing routing site domain aliases is up to date', {
1275
- existedDomainAliases: existSite.domainAliases,
1283
+ // let didDomain in front of ipEchoDnsDomain
1284
+ domainAliases.sort((a, b) => b.value.length - a.value.length);
1285
+ if (isEqual(site.domainAliases, domainAliases)) {
1286
+ logger.info('site domain aliases is up to date', {
1287
+ siteId: site.id,
1288
+ existedDomainAliases: site.domainAliases,
1276
1289
  domainAliases,
1277
1290
  didDomainList,
1278
1291
  });
1279
1292
  } else {
1280
- await states.site.updateDomainAliasList(existSite.id, domainAliases);
1281
- logger.info('update existing routing site domain aliases', { site: existSite, domainAliases, didDomainList });
1293
+ await states.site.updateDomainAliasList(site.id, domainAliases);
1294
+ logger.info('update site domain aliases', { site, domainAliases, didDomainList });
1295
+ return true;
1282
1296
  }
1283
1297
  } catch (error) {
1284
- logger.error('update existing routing site domain aliases failed', { error, existSite });
1298
+ logger.error('update site domain aliases failed', { error, site });
1285
1299
  }
1300
+
1301
+ return false;
1286
1302
  }
1287
1303
 
1288
- /**
1289
- * Add system sites for blocklet
1290
- *
1291
- * @returns {boolean} if routing state db changed
1292
- */
1293
- const _ensureBlockletSites = async (blocklet, nodeInfo, context = {}) => {
1304
+ const _ensureBlockletRouting = async (blocklet, nodeInfo, context = {}) => {
1294
1305
  const webInterface = (blocklet.meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1295
1306
  if (!webInterface) {
1296
1307
  return false;
@@ -1304,36 +1315,21 @@ module.exports = function getRouterHelpers({
1304
1315
  };
1305
1316
 
1306
1317
  const domainGroup = getBlockletDomainGroupName(blocklet.meta.did);
1307
-
1308
- const pathPrefix = getPrefix(webInterface.prefix);
1309
1318
  const rule = {
1310
- from: { pathPrefix },
1319
+ from: { pathPrefix: getPrefix(webInterface.prefix) },
1311
1320
  to: {
1312
1321
  port: findInterfacePortByName(blocklet, webInterface.name),
1313
1322
  did: blocklet.meta.did,
1314
1323
  type: ROUTING_RULE_TYPES.BLOCKLET,
1315
1324
  interfaceName: webInterface.name, // root blocklet interface
1316
1325
  },
1317
- isProtected: true,
1318
1326
  };
1319
1327
 
1320
1328
  const existSite = await states.site.findOne({ domain: domainGroup });
1321
- updateBlockletDocument({
1322
- did: blocklet.appPid,
1323
- nodeInfo,
1324
- teamManager,
1325
- states,
1326
- })
1327
- .then(() => {
1328
- logger.info('updated blocklet dns document', {
1329
- did: blocklet.appPid,
1330
- });
1331
- })
1332
- .catch((err) => {
1333
- logger.error('update blocklet dns document failed', { did: blocklet.meta.did, error: err });
1334
- });
1335
-
1336
- if (!existSite) {
1329
+ if (existSite) {
1330
+ await updateSiteDomainAliases(existSite, blocklet, nodeInfo);
1331
+ logger.info('site already exists', { did: blocklet.meta.did, site: existSite });
1332
+ } else {
1337
1333
  const domainAliases = getBlockletDidDomainList(blocklet, nodeInfo);
1338
1334
  // let didDomain in front of ipEchoDnsDomain
1339
1335
  domainAliases.push({ value: getIpDnsDomainForBlocklet(blocklet), isProtected: true });
@@ -1351,7 +1347,7 @@ module.exports = function getRouterHelpers({
1351
1347
  });
1352
1348
  rules.push(...rulesInPack);
1353
1349
 
1354
- await routerManager.addRoutingSite(
1350
+ const created = await routerManager.addRoutingSite(
1355
1351
  {
1356
1352
  site: {
1357
1353
  domain: domainGroup,
@@ -1364,7 +1360,12 @@ module.exports = function getRouterHelpers({
1364
1360
  },
1365
1361
  context
1366
1362
  );
1367
- logger.info('add routing site', { site: domainGroup });
1363
+ logger.info('create routing site for blocklet', {
1364
+ did: blocklet.meta.did,
1365
+ siteId: created.id,
1366
+ rules,
1367
+ domainAliases,
1368
+ });
1368
1369
 
1369
1370
  const bindDomainCap = await states.blockletExtras.getSettings(blocklet.meta.did, 'bindDomainCap');
1370
1371
  const nftDid = await states.blockletExtras.getSettings(blocklet.meta.did, 'domainNftDid');
@@ -1436,35 +1437,35 @@ module.exports = function getRouterHelpers({
1436
1437
  chainHost: context.domainChainHost,
1437
1438
  });
1438
1439
  }
1439
-
1440
- return true;
1441
1440
  }
1442
1441
 
1443
- logger.info('site already exists', { site: existSite });
1444
-
1445
- await updateSiteDomainAliases(existSite, blocklet, nodeInfo);
1442
+ updateBlockletDocument({
1443
+ did: blocklet.meta.did,
1444
+ nodeInfo,
1445
+ teamManager,
1446
+ states,
1447
+ })
1448
+ .then(() => {
1449
+ logger.info('updated did document succeed on add blocklet', { did: blocklet.meta.did });
1446
1450
 
1447
- const existRule = (existSite.rules || []).find((y) => get(y, 'from.pathPrefix') === pathPrefix);
1448
- if (
1449
- existRule &&
1450
- (rule.to?.type !== ROUTING_RULE_TYPES.BLOCKLET || existRule.to?.type === ROUTING_RULE_TYPES.BLOCKLET)
1451
- ) {
1452
- await routerManager.updateRoutingRule({
1453
- id: existSite.id,
1454
- rule: {
1455
- ...rule,
1456
- id: existRule.id,
1457
- },
1458
- skipProtectedRuleChecking: true,
1459
- });
1460
- logger.info('update routing rule for site', { site: domainGroup });
1461
- } else {
1462
- await routerManager.addRoutingRule({
1463
- id: existSite.id,
1464
- rule,
1451
+ // Trigger DNS lookup to warm up DNS cache (non-blocking)
1452
+ // This helps reduce DNS resolution time when user accesses the blocklet after install
1453
+ const domain = getDidDomainForBlocklet({
1454
+ did: blocklet.meta.did,
1455
+ didDomain: nodeInfo.didDomain || DEFAULT_DID_DOMAIN,
1456
+ });
1457
+ dns
1458
+ .resolve(domain)
1459
+ .then((result) => {
1460
+ logger.info('dns cache warm-up completed', { did: blocklet.meta.did, domain, result });
1461
+ })
1462
+ .catch((error) => {
1463
+ logger.warn('dns cache warm-up failed', { did: blocklet.meta.did, domain, error: error.message });
1464
+ });
1465
+ })
1466
+ .catch((err) => {
1467
+ logger.error('update did document failed on add blocklet', { did: blocklet.meta.did, error: err });
1465
1468
  });
1466
- logger.info('add routing rule for site', { site: domainGroup });
1467
- }
1468
1469
 
1469
1470
  return true;
1470
1471
  };
@@ -1513,12 +1514,7 @@ module.exports = function getRouterHelpers({
1513
1514
 
1514
1515
  try {
1515
1516
  const nodeInfo = await nodeState.read();
1516
- const hasWebInterface = (blocklet.meta.interfaces || []).some((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
1517
- if (!hasWebInterface) {
1518
- return false;
1519
- }
1520
-
1521
- return await _ensureBlockletSites(blocklet, nodeInfo, context);
1517
+ return await _ensureBlockletRouting(blocklet, nodeInfo, context);
1522
1518
  } finally {
1523
1519
  await ensureBlockletRoutingLock.releaseLock(lockName);
1524
1520
  }
@@ -1634,22 +1630,49 @@ module.exports = function getRouterHelpers({
1634
1630
  return ruleChanged || siteChanged;
1635
1631
  };
1636
1632
 
1637
- async function readRoutingSites(snapshotHash) {
1638
- let hash = snapshotHash;
1639
- if (!hash) {
1640
- logger.debug('router.readRoutingSites read snapshot hash from snapshot db');
1641
- hash = await routingSnapshot.getLastSnapshot();
1642
- }
1633
+ async function readAllRoutingSites() {
1634
+ const sites = await siteState.getSites();
1635
+ const blocklets = await states.blocklet.getBlocklets();
1636
+ return {
1637
+ sites: await ensureLatestInfo(sites, blocklets, teamManager),
1638
+ };
1639
+ }
1643
1640
 
1644
- if (!hash) {
1645
- logger.error('Can not determine routing snapshot hash, there must be something wrong!');
1646
- return {};
1641
+ /**
1642
+ * Lightweight version of readAllRoutingSites for a single blocklet
1643
+ * Only queries the specific blocklet's data from database
1644
+ * @param {string} blockletDid - The blocklet DID
1645
+ * @returns {Promise<Array>}
1646
+ */
1647
+ async function readBlockletRoutingSite(blockletDid) {
1648
+ const [site, blocklet] = await Promise.all([
1649
+ siteState.findOneByBlocklet(blockletDid),
1650
+ states.blocklet.getBlocklet(blockletDid),
1651
+ ]);
1652
+
1653
+ if (!site || !blocklet) {
1654
+ logger.warn('readBlockletRoutingSite: site or blocklet not found', {
1655
+ blockletDid,
1656
+ hasSite: !!site,
1657
+ hasBlocklet: !!blocklet,
1658
+ });
1659
+ return [];
1647
1660
  }
1648
1661
 
1649
- const result = await routingSnapshot.readSnapshot(hash);
1650
- result.sites = await ensureLatestInfo(result.sites);
1662
+ return ensureLatestInfo([site], [blocklet], teamManager);
1663
+ }
1651
1664
 
1652
- return result;
1665
+ /**
1666
+ * Lightweight version of readAllRoutingSites for system sites only (no blocklet sites)
1667
+ * O(1) complexity - only queries system sites from database
1668
+ * System sites: dashboard, wellknown, IP site, default site
1669
+ * @returns {Promise<{sites: Array}>}
1670
+ */
1671
+ async function readSystemRoutingSites() {
1672
+ // getSystemSites() returns sites where domain NOT LIKE '%@blocklet-site-group'
1673
+ const systemSites = await siteState.getSystemSites();
1674
+ const sites = await ensureLatestInfo(systemSites, [], teamManager);
1675
+ return { sites };
1653
1676
  }
1654
1677
 
1655
1678
  async function resetSiteByDid(did, { refreshRouterProvider = true } = {}) {
@@ -1658,16 +1681,13 @@ module.exports = function getRouterHelpers({
1658
1681
  await ensureBlockletRouting(blocklet);
1659
1682
  if (refreshRouterProvider) {
1660
1683
  // eslint-disable-next-line no-use-before-define
1661
- const hash = await takeRoutingSnapshot({ message: `Reset blocklet ${did}`, dryRun: false });
1662
- logger.info('reset blocklet routing rules', { did, hash });
1684
+ await handleBlockletRouting({ did, message: 'reset blocklet routing rules' });
1663
1685
  }
1664
1686
  }
1665
1687
 
1666
- const providers = {}; // we need to keep reference for different router instances
1667
-
1668
1688
  const startAccessLogWatcher = (info) => {
1669
1689
  const providerName = get(info, 'routing.provider', null);
1670
- if (!providerName || !providers[providerName] || isInstanceWorker()) {
1690
+ if (!providerName || !providers[providerName] || isWorkerInstance()) {
1671
1691
  return;
1672
1692
  }
1673
1693
 
@@ -1693,7 +1713,7 @@ module.exports = function getRouterHelpers({
1693
1713
 
1694
1714
  const startErrorLogWatcher = async (info, shouldScheduleReload = false) => {
1695
1715
  const providerName = get(info, 'routing.provider', null);
1696
- if (!providerName || !providers[providerName] || isInstanceWorker()) {
1716
+ if (!providerName || !providers[providerName] || isWorkerInstance()) {
1697
1717
  return;
1698
1718
  }
1699
1719
 
@@ -1724,19 +1744,20 @@ module.exports = function getRouterHelpers({
1724
1744
  logger.debug('router error detected', { logEntries, grouped, blocked, timestamp: dayjs().unix() });
1725
1745
 
1726
1746
  if (blocked.length) {
1727
- providers[providerName].throttledReload();
1728
- // Reload 1 second after block expiration
1747
+ // Queue global change to update blacklist in nginx config
1748
+ providers[providerName].queueChange('global');
1749
+ // Schedule reload after block expiration to remove IPs from blacklist
1729
1750
  uniq(blocked).forEach((timeout) => {
1730
1751
  setTimeout(() => {
1731
1752
  logger.info('router reload on block expire', { timeout, timestamp: dayjs().unix() });
1732
- providers[providerName].throttledReload();
1753
+ providers[providerName].queueChange('global');
1733
1754
  }, timeout + 1000);
1734
1755
  });
1735
1756
  }
1736
1757
  });
1737
1758
  }
1738
1759
 
1739
- // Schedule router reload for active blacklist
1760
+ // Schedule router reload for active blacklist expiration
1740
1761
  if (daemon && shouldScheduleReload) {
1741
1762
  const blacklist = await blocker.getActiveBlacklist(false);
1742
1763
  if (blacklist.length) {
@@ -1746,58 +1767,77 @@ module.exports = function getRouterHelpers({
1746
1767
  const timeout = x.expiresAt - now + 1;
1747
1768
  setTimeout(() => {
1748
1769
  logger.info('router reload on block expire', { ip: x.key });
1749
- providers[providerName].throttledReload();
1770
+ providers[providerName].queueChange('global');
1750
1771
  }, timeout * 1000);
1751
1772
  });
1752
1773
  }
1753
1774
  }
1754
1775
  };
1755
1776
 
1756
- const handleRouting = async (nodeInfo) => {
1757
- const now = Date.now();
1758
- logger.info('start handle routing', {
1759
- snapshotHash: nodeInfo?.routing?.snapshotHash,
1760
- });
1761
- const providerName = get(nodeInfo, 'routing.provider', null);
1777
+ const providers = {}; // we need to keep reference for different router instances
1778
+ const providerInitPromises = {}; // track ongoing initializations for race condition handling
1779
+
1780
+ /**
1781
+ * Ensure routing provider is initialized (idempotent, race-condition safe)
1782
+ * Multiple concurrent calls will await the same initialization promise
1783
+ * @returns {Promise<{ provider: Router, nodeInfo: object, providerName: string }>}
1784
+ */
1785
+ const ensureRoutingProvider = async () => {
1786
+ const nodeInfo = await nodeState.read();
1787
+ const providerName = get(nodeInfo, 'routing.provider');
1762
1788
  const httpsEnabled = get(nodeInfo, 'routing.https', true);
1763
- logger.debug('handle routing', { providerName, httpsEnabled });
1764
1789
 
1765
- const Provider = getProvider(providerName);
1766
- const checkResult = await Provider.check({ configDir: dataDirs.router });
1767
- if (!checkResult.available) {
1768
- throw new Error(`${providerName} pre-check failed, ${checkResult.error}`);
1790
+ // Already initialized - fast path
1791
+ if (providers[providerName]) {
1792
+ return { provider: providers[providerName], nodeInfo, providerName };
1769
1793
  }
1770
1794
 
1771
- const shouldScheduleReload = !providers[providerName];
1795
+ // Already initializing - await the same promise
1796
+ if (providerInitPromises[providerName]) {
1797
+ await providerInitPromises[providerName];
1798
+ return { provider: providers[providerName], nodeInfo, providerName };
1799
+ }
1800
+
1801
+ // Start initialization - store promise so concurrent calls can await it
1802
+ providerInitPromises[providerName] = (async () => {
1803
+ const startedAt = Date.now();
1804
+ logger.info('ensureRoutingProvider: initializing', { providerName, httpsEnabled });
1805
+
1806
+ const Provider = getProvider(providerName);
1807
+ const checkResult = await Provider.check({ configDir: dataDirs.router });
1808
+ if (!checkResult.available) {
1809
+ throw new Error(`routing provider ${providerName} pre-check failed, ${checkResult.error}`);
1810
+ }
1772
1811
 
1773
- if (providers[providerName]) {
1774
- await providers[providerName].reload();
1775
- } else {
1776
1812
  providers[providerName] = new Router({
1777
1813
  provider: createProviderInstance({ nodeInfo, routerDataDir: dataDirs.router }),
1778
- getRoutingParams: async () => {
1814
+ getAllRoutingParams: async () => {
1779
1815
  try {
1780
- const info = await nodeState._read();
1781
- logger.info('router:getRoutingParams read routing params', { snapshotHash: info.routing?.snapshotHash });
1782
-
1783
- let { sites } = await readRoutingSites(info.routing?.snapshotHash);
1784
- sites = await ensureLatestInfo(sites);
1785
- sites = await ensureServiceRule(sites);
1786
- sites = await ensureRootRule(sites);
1787
-
1788
- const certificates = httpsEnabled ? await certManager.getAllNormal() : [];
1816
+ // Parallelize all independent async operations
1817
+ const [info, { sites }, services, certificates, wafDisabledList] = await Promise.all([
1818
+ nodeState._read(),
1819
+ readAllRoutingSites(),
1820
+ blockletState.getServices(),
1821
+ httpsEnabled ? certManager.getAllNormal() : Promise.resolve([]),
1822
+ states.blockletExtras.getWafDisabledBlocklets(),
1823
+ ]);
1824
+
1825
+ // Fetch site info for WAF disabled blocklets in parallel
1826
+ const wafDisabledBlocklets = await Promise.all(
1827
+ wafDisabledList.map((x) =>
1828
+ states.site.findOneByBlocklet(x.did).then((result) => ({ did: x.did, site: result }))
1829
+ )
1830
+ );
1831
+
1832
+ logger.info('router:getAllRoutingParams read routing params', { services });
1789
1833
 
1790
1834
  return {
1791
- sites,
1835
+ sites: await ensureRootRule(sites),
1792
1836
  certificates,
1793
1837
  headers: get(nodeInfo, 'routing.headers', {}),
1794
- services: await blockletState.getServices(),
1838
+ services,
1795
1839
  nodeInfo: info,
1796
- wafDisabledBlocklets: await Promise.all(
1797
- (await states.blockletExtras.getWafDisabledBlocklets()).map((x) =>
1798
- states.site.findOneByBlocklet(x.did).then((result) => ({ did: x.did, site: result }))
1799
- )
1800
- ),
1840
+ wafDisabledBlocklets,
1801
1841
  };
1802
1842
  } catch (err) {
1803
1843
  logger.error('Read routing rules failed', { error: err });
@@ -1812,8 +1852,98 @@ module.exports = function getRouterHelpers({
1812
1852
  return {};
1813
1853
  }
1814
1854
  },
1855
+ // Lightweight getter for single blocklet updates - O(1) database queries
1856
+ getBlockletRoutingParams: async (blockletDid) => {
1857
+ try {
1858
+ // Parallelize all independent async operations
1859
+ const [info, sites, certificates] = await Promise.all([
1860
+ nodeState._read(),
1861
+ readBlockletRoutingSite(blockletDid),
1862
+ httpsEnabled ? certManager.getAllNormal() : Promise.resolve([]),
1863
+ ]);
1864
+
1865
+ if (!sites || sites.length === 0) {
1866
+ logger.warn('router: getBlockletRoutingParams empty', { blockletDid });
1867
+ return null;
1868
+ }
1869
+
1870
+ logger.info('router: getBlockletRoutingParams success', { blockletDid, sites });
1871
+
1872
+ return {
1873
+ sites,
1874
+ certificates,
1875
+ headers: get(info, 'routing.headers', {}),
1876
+ nodeInfo: info,
1877
+ wafDisabledBlocklets: [],
1878
+ };
1879
+ } catch (err) {
1880
+ logger.error('router: getBlockletRoutingParams failed', { blockletDid, error: err });
1881
+ return null;
1882
+ }
1883
+ },
1884
+ // Lightweight getter for global/system routing params - O(1) database queries
1885
+ // Returns only system sites (dashboard, wellknown, IP, default) + services + global policies
1886
+ getSystemRoutingParams: async () => {
1887
+ try {
1888
+ // Parallelize all independent async operations
1889
+ const [info, { sites }, services, certificates, wafDisabledList] = await Promise.all([
1890
+ nodeState._read(),
1891
+ readSystemRoutingSites(),
1892
+ blockletState.getServices(),
1893
+ httpsEnabled ? certManager.getAllNormal() : Promise.resolve([]),
1894
+ states.blockletExtras.getWafDisabledBlocklets(),
1895
+ ]);
1896
+
1897
+ // Fetch site info for WAF disabled blocklets in parallel
1898
+ const wafDisabledBlocklets = await Promise.all(
1899
+ wafDisabledList.map((x) =>
1900
+ states.site.findOneByBlocklet(x.did).then((result) => ({ did: x.did, site: result }))
1901
+ )
1902
+ );
1903
+
1904
+ logger.info('router: getSystemRoutingParams success', {
1905
+ services,
1906
+ wafDisabledBlocklets,
1907
+ sites,
1908
+ });
1909
+
1910
+ return {
1911
+ sites,
1912
+ certificates,
1913
+ services,
1914
+ headers: get(info, 'routing.headers', {}),
1915
+ nodeInfo: info,
1916
+ wafDisabledBlocklets,
1917
+ };
1918
+ } catch (err) {
1919
+ logger.error('router: getSystemRoutingParams failed', { error: err });
1920
+ return null;
1921
+ }
1922
+ },
1815
1923
  });
1816
1924
 
1925
+ // Helper to find affected blocklets by certificate
1926
+ const findAffectedBlockletsByCert = async (cert) => {
1927
+ if (!cert) return [];
1928
+
1929
+ const sites = await siteState.getSites();
1930
+ const affectedDids = new Set();
1931
+
1932
+ for (const site of sites) {
1933
+ if (!site.domain.endsWith(BLOCKLET_SITE_GROUP_SUFFIX)) continue;
1934
+
1935
+ const domains = [site.domain, ...(site.domainAliases || []).map((x) => x.value)];
1936
+ const isMatch = domains.some((domain) => domain && routerManager.isCertMatchedDomain(cert, domain));
1937
+
1938
+ if (isMatch) {
1939
+ affectedDids.add(site.domain.replace(BLOCKLET_SITE_GROUP_SUFFIX, ''));
1940
+ }
1941
+ }
1942
+
1943
+ return Array.from(affectedDids);
1944
+ };
1945
+
1946
+ // Set up cert event handlers - trigger targeted routing updates
1817
1947
  [
1818
1948
  BlockletEvents.certIssued,
1819
1949
  EVENTS.CERT_ADDED,
@@ -1821,16 +1951,85 @@ module.exports = function getRouterHelpers({
1821
1951
  EVENTS.CERT_ISSUED,
1822
1952
  EVENTS.CERT_UPDATED,
1823
1953
  ].forEach((event) => {
1824
- certManager.on(event, () => providers[providerName].reload());
1954
+ certManager.on(event, async (cert) => {
1955
+ if (!cert) return;
1956
+
1957
+ logger.info('cert event triggered routing update', { event, domain: cert.domain });
1958
+
1959
+ // Always update global routing for cert changes
1960
+ // eslint-disable-next-line no-use-before-define
1961
+ await handleSystemRouting({ message: `cert event: ${event}` });
1962
+
1963
+ // Find and update affected blocklets
1964
+ const affectedDids = await findAffectedBlockletsByCert(cert);
1965
+ for (const did of affectedDids) {
1966
+ // eslint-disable-next-line no-await-in-loop, no-use-before-define
1967
+ await handleBlockletRouting({ did, message: `cert event: ${event}` });
1968
+ }
1969
+ });
1825
1970
  });
1826
1971
 
1827
- await providers[providerName].start();
1972
+ await startAccessLogWatcher(nodeInfo);
1973
+ await startErrorLogWatcher(nodeInfo, true);
1974
+
1975
+ logger.info('ensureRoutingProvider: initialized', { providerName, duration: Date.now() - startedAt });
1976
+ })();
1977
+
1978
+ try {
1979
+ await providerInitPromises[providerName];
1980
+ } finally {
1981
+ // Clean up promise regardless of success/failure
1982
+ delete providerInitPromises[providerName];
1828
1983
  }
1829
1984
 
1830
- await startAccessLogWatcher(nodeInfo);
1831
- await startErrorLogWatcher(nodeInfo, shouldScheduleReload);
1985
+ return { provider: providers[providerName], nodeInfo, providerName };
1986
+ };
1832
1987
 
1833
- logger.info('done handle routing', { cost: Date.now() - now });
1988
+ /**
1989
+ * Lightweight handler for global/system routing updates - O(1) complexity
1990
+ * Use this for changes that only affect global config (policies, system sites, services)
1991
+ * @param {Object} options
1992
+ * @param {string} [options.message] - Log message describing the change
1993
+ */
1994
+ const handleSystemRouting = async ({ message = '' } = {}) => {
1995
+ logger.info('handleSystemRouting: triggered', { message });
1996
+ const { provider, providerName } = await ensureRoutingProvider();
1997
+ provider.queueChange('global');
1998
+ logger.info('handleSystemRouting: queued', { message, providerName });
1999
+ };
2000
+
2001
+ /**
2002
+ * Lightweight handler for single blocklet routing updates - O(2) complexity
2003
+ * Updates blocklet config + global config (for services that may have changed)
2004
+ * @param {Object} options
2005
+ * @param {string} options.did - Blocklet DID
2006
+ * @param {string} [options.message] - Log message describing the change
2007
+ * @param {boolean} [options.isRemoval] - Whether this is a blocklet removal
2008
+ */
2009
+ const handleBlockletRouting = async ({ did, message = '', isRemoval = false }) => {
2010
+ logger.info('handleBlockletRouting: triggered', { did, message, isRemoval });
2011
+ if (!did) {
2012
+ return;
2013
+ }
2014
+
2015
+ const { provider, providerName } = await ensureRoutingProvider();
2016
+ const changeType = isRemoval ? 'blocklet-remove' : 'blocklet';
2017
+ provider.queueChange('global');
2018
+ provider.queueChange(changeType, did);
2019
+ logger.info('handleBlockletRouting: queued', { did, message, isRemoval, providerName });
2020
+ };
2021
+
2022
+ /**
2023
+ * Full regeneration handler - O(N) complexity
2024
+ * Use this for startup, manual rebuild, or when complete regeneration is needed
2025
+ * @param {Object} options
2026
+ * @param {string} [options.message] - Log message describing the change
2027
+ */
2028
+ const handleAllRouting = async ({ message = '' } = {}) => {
2029
+ logger.info('handleAllRouting: triggered', { message });
2030
+ const { provider, providerName } = await ensureRoutingProvider();
2031
+ await provider.regenerateAll({ message });
2032
+ logger.info('handleAllRouting: done', { message, providerName });
1834
2033
  };
1835
2034
 
1836
2035
  const rotateRouterLog = async () => {
@@ -1846,8 +2045,6 @@ module.exports = function getRouterHelpers({
1846
2045
 
1847
2046
  const analyzeRouterLog = async () => {
1848
2047
  const info = await nodeState.read();
1849
- // eslint-disable-next-line no-use-before-define
1850
- const sites = await getRoutingSites({});
1851
2048
  const providerName = get(info, 'routing.provider', null);
1852
2049
  if (!providerName || !providers[providerName]) {
1853
2050
  logger.warn('No router provider instance found');
@@ -1855,6 +2052,8 @@ module.exports = function getRouterHelpers({
1855
2052
  }
1856
2053
 
1857
2054
  const groups = [];
2055
+ const sites = await attachRuntimeDomainAliases({ sites: await siteState.getSites(), context: {} });
2056
+
1858
2057
  const server = sites.find((x) =>
1859
2058
  x.rules.some((rule) => rule.to.type === ROUTING_RULE_TYPES.DAEMON && rule.to.did === info.did)
1860
2059
  );
@@ -1935,124 +2134,10 @@ module.exports = function getRouterHelpers({
1935
2134
  logger.info('refresh gateway blacklist from cron', { hash, blacklist });
1936
2135
  };
1937
2136
 
1938
- const updateNodeRouting = async (params, context = {}) => {
1939
- const info = await nodeState.read();
1940
- const { snapshotHash: oldSnapshotHash } = info.routing || {};
1941
- const { snapshotHash: newSnapshotHash, forceRepopulate = false } = params;
1942
-
1943
- // Repopulate routing rules if we have changed the routing snapshot
1944
- const isValidSnapshot = newSnapshotHash ? await routingSnapshot.hasSnapshot(newSnapshotHash) : false;
1945
- if (isValidSnapshot) {
1946
- if (!oldSnapshotHash || oldSnapshotHash !== newSnapshotHash || forceRepopulate) {
1947
- const { sites, meta } = await routingSnapshot.readSnapshot(newSnapshotHash);
1948
- const result = await routerManager.repopulateRouting({ sites });
1949
- await routingSnapshot.restoreMetaData(meta);
1950
- logger.info(`repopulate routing rules from snapshot: ${newSnapshotHash}`, { result });
1951
- }
1952
- }
1953
-
1954
- const newProvider = params.provider || getProviderFromNodeInfo(info);
1955
- const result = await nodeState.updateNodeRouting({
1956
- ...info.routing,
1957
- snapshotHash: newSnapshotHash || oldSnapshotHash || '',
1958
- provider: newProvider,
1959
- });
1960
- await handleRouting(result);
1961
-
1962
- if (newProvider !== info.routing.provider) {
1963
- // Ensure we have system sites for daemon
1964
- await ensureDashboardRouting(context);
1965
-
1966
- // Ensure we have system rules for blocklets
1967
- const blocklets = await blockletState.getBlocklets();
1968
- const ensureBlocklet = async (x) => {
1969
- const blocklet = await blockletManager.getBlocklet(x.meta.did);
1970
- return ensureBlockletRouting(blocklet, context);
1971
- };
1972
- const ensureBlockletResults = await Promise.all(blocklets.map((x) => ensureBlocklet(x)));
1973
-
1974
- // We need to take snapshot after system rules ensured
1975
- if (ensureBlockletResults.filter(Boolean).length) {
1976
- // eslint-disable-next-line no-use-before-define
1977
- await takeRoutingSnapshot({ message: `Switch routing engine to ${newProvider}`, dryRun: false }, context);
1978
- logger.info(`take routing snapshot on switch engine: ${newProvider}`, { ensureBlockletResults });
1979
- }
1980
-
1981
- const Provider = getProvider(info.routing.provider);
1982
- if (Provider) {
1983
- try {
1984
- const providerInstance = createProviderInstance({
1985
- nodeInfo: info,
1986
- routerDataDir: dataDirs.router,
1987
- });
1988
-
1989
- await providerInstance.stop();
1990
- logger.info('original router stopped:', { provider: info.routing.provider });
1991
- } catch (err) {
1992
- logger.error('original router stop failed:', { provider: info.routing.provider, error: err });
1993
- }
1994
- }
1995
- }
1996
-
1997
- return result;
1998
- };
1999
-
2000
- /**
2001
- * @param {string} message
2002
- * @param {boolean} dryRun
2003
- * @param {boolean} handleRouting if NOT dryRun and handleRouting is true, will run handleRouting() after update routing
2004
- * @returns
2005
- */
2006
- // eslint-disable-next-line no-unused-vars
2007
- const takeRoutingSnapshot = async ({ message, dryRun = true, handleRouting: handle = true }, context = {}) => {
2008
- const msg = decodeURIComponent(message);
2009
-
2010
- if (!msg) {
2011
- throw new Error('Please provide a message for this snapshot');
2012
- }
2013
- if (!dryRun) {
2014
- if (msg.length < 5) {
2015
- throw new Error('Message cannot be less than 5 characters');
2016
- }
2017
- if (msg.length > 150) {
2018
- throw new Error('Message cannot exceed 100 characters');
2019
- }
2020
- }
2021
-
2022
- const hash = await routingSnapshot.takeSnapshot(msg, dryRun);
2023
- if (!dryRun) {
2024
- let nodeInfo = await nodeState.read();
2025
- nodeInfo = await nodeState.updateNodeRouting({ ...nodeInfo.routing, snapshotHash: hash });
2026
- if (handle) {
2027
- await handleRouting(nodeInfo);
2028
- }
2029
- logger.info('takeRoutingSnapshot', {
2030
- dryRun,
2031
- handleRouting: handle,
2032
- hash,
2033
- dbSnapshotHash: nodeInfo?.routing?.snapshotHash,
2034
- });
2035
- }
2036
-
2037
- return hash;
2038
- };
2039
-
2040
- const getSitesFromSnapshot = async ({ snapshotHash } = {}) => {
2041
- const latestSnapshot = await routingSnapshot.getLastSnapshot(true);
2042
- if (!snapshotHash || snapshotHash === latestSnapshot) {
2043
- const sites = await siteState.getSites();
2044
- return ensureLatestInfo(sites);
2045
- }
2046
-
2047
- const isValidSnapshot = await routingSnapshot.hasSnapshot(snapshotHash);
2048
- if (!isValidSnapshot) {
2049
- const sites = await siteState.getSites();
2050
- return ensureLatestInfo(sites);
2051
- }
2052
-
2053
- const { sites = [] } = await routingSnapshot.readSnapshot(snapshotHash);
2054
-
2055
- return ensureLatestInfo(sites);
2137
+ const getSitesFromState = async (scope = 'all') => {
2138
+ const sites = scope === 'all' ? await siteState.getSites() : await siteState.getSystemSites();
2139
+ const blocklets = scope === 'all' ? await states.blocklet.getBlocklets() : [];
2140
+ return ensureLatestInfo(sites, blocklets, teamManager);
2056
2141
  };
2057
2142
 
2058
2143
  /**
@@ -2061,30 +2146,22 @@ module.exports = function getRouterHelpers({
2061
2146
  * @param {*} context
2062
2147
  * @param {string} withInterfaceUrls
2063
2148
  */
2064
- const getRoutingSites = async (params, context = {}, { withInterfaceUrls = true, withDefaultCors = true } = {}) => {
2065
- const sites = await getSitesFromSnapshot();
2149
+ const getRoutingSites = async (params, context = {}, { withInterfaceUrls = true, scope = 'all' } = {}) => {
2150
+ const sites = await getSitesFromState(scope);
2066
2151
 
2067
2152
  if (!withInterfaceUrls) {
2068
2153
  return sites;
2069
2154
  }
2070
2155
 
2071
2156
  return attachRuntimeDomainAliases({
2072
- sites: await ensureLatestInfo(sites, { withDefaultCors }),
2073
- context,
2074
- });
2075
- };
2076
-
2077
- const getSnapshotSites = async ({ hash }, context = {}, { withDefaultCors = true } = {}) => {
2078
- const sites = await routingSnapshot.readSnapshotSites(hash);
2079
- return attachRuntimeDomainAliases({
2080
- sites: await ensureLatestInfo(sites, { withDefaultCors }),
2157
+ sites,
2081
2158
  context,
2082
2159
  });
2083
2160
  };
2084
2161
 
2085
2162
  const getCertificates = async () => {
2086
2163
  const certificates = await certManager.getAll();
2087
- const sites = await getSitesFromSnapshot();
2164
+ const sites = await attachRuntimeDomainAliases({ sites: await siteState.getSites(), context: {} });
2088
2165
 
2089
2166
  const isMatch = (cert, domain) =>
2090
2167
  domain !== DOMAIN_FOR_DEFAULT_SITE && domain && routerManager.isCertMatchedDomain(cert, domain);
@@ -2105,18 +2182,14 @@ module.exports = function getRouterHelpers({
2105
2182
  };
2106
2183
 
2107
2184
  /**
2108
- * proxy to routerManager and do takeRoutingSnapshot
2185
+ * proxy to routerManager and trigger handleBlockletRouting after operation
2109
2186
  */
2110
2187
  const _proxyToRouterManager =
2111
2188
  (fnName) =>
2112
2189
  async (...args) => {
2113
2190
  const res = await routerManager[fnName](...args);
2114
-
2115
- takeRoutingSnapshot({ message: fnName, dryRun: false }).catch((error) => {
2116
- logger.error('failed to takeRoutingSnapshot', { error });
2117
- });
2118
-
2119
2191
  const { teamDid: did } = args[0] || {};
2192
+ await handleBlockletRouting({ did, message: fnName });
2120
2193
  if (did) {
2121
2194
  routerManager.emit(BlockletEvents.updated, { meta: { did } });
2122
2195
  }
@@ -2182,13 +2255,12 @@ module.exports = function getRouterHelpers({
2182
2255
  ensureBlockletRouting,
2183
2256
  ensureBlockletRoutingForUpgrade,
2184
2257
  removeBlockletRouting,
2185
- handleRouting,
2258
+ handleSystemRouting, // O(1) - for global config changes only
2259
+ handleBlockletRouting, // O(2) - for single blocklet + global (services)
2260
+ handleAllRouting, // O(N) - for full regeneration
2186
2261
  resetSiteByDid,
2187
- updateNodeRouting,
2188
- takeRoutingSnapshot,
2189
2262
  getRoutingSites,
2190
- getSnapshotSites,
2191
- getSitesFromSnapshot,
2263
+ getSitesFromState,
2192
2264
  getCertificates,
2193
2265
  ensureWildcardCerts,
2194
2266
  ensureServerlessCerts,
@@ -2267,6 +2339,7 @@ module.exports.ensureLatestInfo = ensureLatestInfo;
2267
2339
  module.exports.ensureWellknownRule = ensureWellknownRule;
2268
2340
  module.exports.ensureBlockletWellknownRules = ensureBlockletWellknownRules;
2269
2341
  module.exports.ensureBlockletProxyBehavior = ensureBlockletProxyBehavior;
2342
+ module.exports.ensureBlockletStaticServing = ensureBlockletStaticServing;
2270
2343
  module.exports.expandComponentRules = expandComponentRules;
2271
2344
  module.exports.getDomainsByDid = getDomainsByDid;
2272
2345
  module.exports.ensureBlockletHasMultipleInterfaces = ensureBlockletHasMultipleInterfaces;