@abtnode/core 1.17.10 → 1.17.11-beta-20260225-043848-68611a07

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.
@@ -184,6 +184,11 @@ async function config(manager, { did, configs: newConfigs, skipHook, skipDidDocu
184
184
  manager.emit(BlockletEvents.spaceConnected, blocklet);
185
185
  }
186
186
 
187
+ // Reload nginx when APP_NO_INDEX changes to update X-Robots-Tag header
188
+ if (finalConfigs.find((x) => x.key === 'APP_NO_INDEX')) {
189
+ manager.emit(BlockletEvents.gatewayConfigChanged, blocklet);
190
+ }
191
+
187
192
  // update blocklet meta
188
193
  if (blocklet.structVersion && !childDid) {
189
194
  const changedTitle = finalConfigs.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME)?.value;
@@ -98,14 +98,16 @@ async function _installBlocklet(manager, { did, oldBlocklet, componentDids, cont
98
98
  // Add initialize authentication settings
99
99
  await manager.migrateBlockletAuthentication({ did });
100
100
 
101
- // pre install
102
- await manager._runUserHook('preInstall', blocklet, context);
101
+ if (!context.skipHooks) {
102
+ // pre install
103
+ await manager._runUserHook('preInstall', blocklet, context);
103
104
 
104
- // post install
105
- await manager._runUserHook('postInstall', blocklet, context);
105
+ // post install
106
+ await manager._runUserHook('postInstall', blocklet, context);
106
107
 
107
- // pre flight
108
- await manager._runUserHook('preFlight', blocklet, context);
108
+ // pre flight
109
+ await manager._runUserHook('preFlight', blocklet, context);
110
+ }
109
111
 
110
112
  await states.blocklet.setInstalledAt(did);
111
113
 
@@ -38,6 +38,7 @@ const states = require('../../states');
38
38
  const BaseBlockletManager = require('./base');
39
39
  const { BlockletRuntimeMonitor } = require('../../monitor/blocklet-runtime-monitor');
40
40
  const { SpacesBackup } = require('../storage/backup/spaces');
41
+ const { BlockletImporter } = require('../storage/import/importer');
41
42
  const { installApplicationFromDev } = require('./helper/install-application-from-dev');
42
43
  const { installComponentFromDev } = require('./helper/install-component-from-dev');
43
44
  const { diff } = require('./helper/install-component-from-upload');
@@ -449,6 +450,29 @@ class DiskBlockletManager extends BaseBlockletManager {
449
450
  throw new Error('Can only restore from spaces or disk');
450
451
  }
451
452
 
453
+ /**
454
+ * Import a blocklet from an export directory
455
+ * @param {{ inputDir: string, overwrite?: boolean }} input
456
+ * @param {Record<string, any>} context
457
+ */
458
+ async importBlocklet(input, context = {}) {
459
+ if (input.overwrite && input.blockletDid) {
460
+ const existing = await this.getBlocklet(input.blockletDid);
461
+ if (existing) {
462
+ await this.delete({ did: input.blockletDid, keepData: false, keepLogsDir: false, keepConfigs: false }, context);
463
+ }
464
+ }
465
+
466
+ const importer = new BlockletImporter({
467
+ inputDir: input.inputDir,
468
+ manager: this,
469
+ states,
470
+ context,
471
+ progressCallback: input.progressCallback,
472
+ });
473
+ return importer.import();
474
+ }
475
+
452
476
  /**
453
477
  *
454
478
  * @param {import('@blocklet/server-js').RequestBlockletInput} param0
@@ -47,7 +47,9 @@ const installApplicationFromBackup = async ({
47
47
  throw new Error(`dir(${dir}) does not exist`);
48
48
  }
49
49
 
50
- context.startImmediately = true;
50
+ if (context.startImmediately === undefined) {
51
+ context.startImmediately = true;
52
+ }
51
53
 
52
54
  // parse data from source dir
53
55
  const srcBundleDirs = await getAppDirs(path.join(dir, 'blocklets'));
@@ -39100,7 +39100,7 @@ exports.isSystemRole = isSystemRole;
39100
39100
  /***/ ((module) => {
39101
39101
 
39102
39102
  "use strict";
39103
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.17.9","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.17.9","@abtnode/auth":"1.17.9","@abtnode/certificate-manager":"1.17.9","@abtnode/constant":"1.17.9","@abtnode/cron":"1.17.9","@abtnode/db-cache":"1.17.9","@abtnode/docker-utils":"1.17.9","@abtnode/logger":"1.17.9","@abtnode/models":"1.17.9","@abtnode/queue":"1.17.9","@abtnode/rbac":"1.17.9","@abtnode/router-provider":"1.17.9","@abtnode/util":"1.17.9","@arcblock/did":"1.29.8","@arcblock/did-connect-js":"1.29.8","@arcblock/did-ext":"1.29.8","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"1.29.8","@arcblock/event-hub":"1.29.8","@arcblock/jwt":"1.29.8","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"1.29.8","@arcblock/vc":"1.29.8","@blocklet/constant":"1.17.9","@blocklet/did-space-js":"^1.2.16","@blocklet/env":"1.17.9","@blocklet/error":"^0.3.5","@blocklet/meta":"1.17.9","@blocklet/resolver":"1.17.9","@blocklet/sdk":"1.17.9","@blocklet/server-js":"1.17.9","@blocklet/store":"1.17.9","@blocklet/theme":"^3.5.2","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.29.8","@ocap/util":"1.29.8","@ocap/wallet":"1.29.8","@slack/webhook":"^7.0.6","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","p-wait-for":"^3.2.0","private-ip":"^2.3.4","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","slugify":"^1.6.6","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^11.1.0","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"axios-mock-adapter":"^2.1.0","expand-tilde":"^2.0.2","express":"^4.18.2","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
39103
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.17.10","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.17.10","@abtnode/auth":"1.17.10","@abtnode/certificate-manager":"1.17.10","@abtnode/constant":"1.17.10","@abtnode/cron":"1.17.10","@abtnode/db-cache":"1.17.10","@abtnode/docker-utils":"1.17.10","@abtnode/logger":"1.17.10","@abtnode/models":"1.17.10","@abtnode/queue":"1.17.10","@abtnode/rbac":"1.17.10","@abtnode/router-provider":"1.17.10","@abtnode/util":"1.17.10","@arcblock/did":"1.29.8","@arcblock/did-connect-js":"1.29.8","@arcblock/did-ext":"1.29.8","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"1.29.8","@arcblock/event-hub":"1.29.8","@arcblock/jwt":"1.29.8","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"1.29.8","@arcblock/vc":"1.29.8","@blocklet/constant":"1.17.10","@blocklet/did-space-js":"^1.2.16","@blocklet/env":"1.17.10","@blocklet/error":"^0.3.5","@blocklet/meta":"1.17.10","@blocklet/resolver":"1.17.10","@blocklet/sdk":"1.17.10","@blocklet/server-js":"1.17.10","@blocklet/store":"1.17.10","@blocklet/theme":"^3.5.2","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.29.8","@ocap/util":"1.29.8","@ocap/wallet":"1.29.8","@slack/webhook":"^7.0.6","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","p-wait-for":"^3.2.0","private-ip":"^2.3.4","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","slugify":"^1.6.6","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^11.1.0","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"axios-mock-adapter":"^2.1.0","expand-tilde":"^2.0.2","express":"^4.18.2","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
39104
39104
 
39105
39105
  /***/ }),
39106
39106
 
@@ -0,0 +1,29 @@
1
+ const { BlockletBackup } = require('../backup/blocklet');
2
+
3
+ class BlockletExport extends BlockletBackup {
4
+ ensureParams(backup) {
5
+ super.ensureParams(backup);
6
+ // Capture the wallet-derived appDid from the exporter
7
+ this.exportAppDid = backup.appDid;
8
+ }
9
+
10
+ cleanData() {
11
+ const clone = super.cleanData();
12
+ // Update appDid/appPid to match wallet address derived from appSk
13
+ // Without this, blocklet.json would have appDid = meta.did, which differs
14
+ // from the wallet address that installApplicationFromBackup expects
15
+ if (this.exportAppDid) {
16
+ clone.appDid = this.exportAppDid;
17
+ clone.appPid = this.exportAppDid;
18
+ }
19
+ return clone;
20
+ }
21
+
22
+ // No-op: keep migratedFrom.appSk as plaintext for cross-server export
23
+ // eslint-disable-next-line class-methods-use-this
24
+ encrypt(info) {
25
+ return info;
26
+ }
27
+ }
28
+
29
+ module.exports = { BlockletExport };
@@ -0,0 +1,27 @@
1
+ const { readFileSync } = require('fs-extra');
2
+ const isEmpty = require('lodash/isEmpty');
3
+ const { join } = require('path');
4
+ const security = require('@abtnode/util/lib/security');
5
+
6
+ const { BlockletExtrasBackup } = require('../backup/blocklet-extras');
7
+
8
+ class BlockletExtrasExport extends BlockletExtrasBackup {
9
+ /**
10
+ * Decrypt secure configs to plaintext using .sock key.
11
+ * Keep config.secure = true so that import-side encryptSecurityData() re-encrypts correctly.
12
+ */
13
+ encrypt(configs) {
14
+ if (isEmpty(configs)) {
15
+ return;
16
+ }
17
+
18
+ const dk = readFileSync(join(this.serverDir, '.sock'));
19
+ for (const config of configs) {
20
+ if (config.secure) {
21
+ config.value = security.decrypt(config.value, this.blocklet.meta.did, dk);
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ module.exports = { BlockletExtrasExport };
@@ -0,0 +1,50 @@
1
+ const fs = require('fs-extra');
2
+ const { join } = require('path');
3
+ const { BlockletsBackup } = require('../backup/blocklets');
4
+ const { getBundleDir } = require('../../../util/blocklet');
5
+
6
+ /**
7
+ * Export ALL component bundles (including registry children).
8
+ * Unlike BlockletsBackup which filters out HTTP-URL bundles,
9
+ * this exports everything so the import doesn't depend on the store.
10
+ */
11
+ class BlockletsExport extends BlockletsBackup {
12
+ async export() {
13
+ const metas = this._getAllComponentMetas(this.blocklet);
14
+ const serverBlockletsDir = join(this.serverDir, 'blocklets');
15
+
16
+ /** @type {import('../backup/blocklets').DirToZipMeta[]} */
17
+ const dirs = [];
18
+ for (const meta of metas) {
19
+ const sourceDir = getBundleDir(serverBlockletsDir, meta);
20
+ if (!fs.existsSync(sourceDir)) {
21
+ continue;
22
+ }
23
+ const zipPath = join(this.backupDir, 'blocklets', meta.bundleName || meta.name, `${meta.version}.zip`);
24
+ dirs.push({ sourceDir, zipPath });
25
+ }
26
+ await this.dirsToZip(dirs);
27
+
28
+ return { dirs };
29
+ }
30
+
31
+ /**
32
+ * Collect full meta objects for all components (parent + children, recursive).
33
+ * Returns the meta object directly so getBundleDir can compute the correct path.
34
+ */
35
+ _getAllComponentMetas(blocklet) {
36
+ if (!blocklet?.meta?.bundleName || !blocklet?.meta?.version) {
37
+ return [];
38
+ }
39
+
40
+ const metas = [blocklet.meta];
41
+
42
+ for (const child of blocklet.children || []) {
43
+ metas.push(...this._getAllComponentMetas(child));
44
+ }
45
+
46
+ return metas;
47
+ }
48
+ }
49
+
50
+ module.exports = { BlockletsExport };
@@ -0,0 +1,34 @@
1
+ const { outputJson } = require('fs-extra');
2
+ const { join } = require('path');
3
+
4
+ const EXPORT_META_VERSION = 1;
5
+ const EXPORT_META_FILENAME = 'export-meta.json';
6
+
7
+ /**
8
+ * @param {object} params
9
+ * @param {string} params.outDir - export output directory
10
+ * @param {object} params.blocklet - blocklet state
11
+ * @param {string} params.serverDid - source server DID
12
+ * @param {string} params.appSk - plaintext application secret key
13
+ * @param {string} params.appDid - application DID
14
+ * @returns {Promise<object>} the written meta object
15
+ */
16
+ async function writeExportMeta({ outDir, blocklet, serverDid, appSk, appDid }) {
17
+ const meta = {
18
+ version: EXPORT_META_VERSION,
19
+ exportedAt: new Date().toISOString(),
20
+ sourceServerDid: serverDid,
21
+ blockletDid: blocklet.meta.did,
22
+ blockletName: blocklet.meta.name,
23
+ blockletTitle: blocklet.meta.title,
24
+ blockletVersion: blocklet.meta.version,
25
+ structVersion: blocklet.structVersion,
26
+ appSk,
27
+ appDid,
28
+ };
29
+
30
+ await outputJson(join(outDir, EXPORT_META_FILENAME), meta, { spaces: 2 });
31
+ return meta;
32
+ }
33
+
34
+ module.exports = { writeExportMeta, EXPORT_META_VERSION, EXPORT_META_FILENAME };
@@ -0,0 +1,140 @@
1
+ const path = require('path');
2
+ const { isValid } = require('@arcblock/did');
3
+ const { ensureDirSync } = require('fs-extra');
4
+ const isEmpty = require('lodash/isEmpty');
5
+ const { getBlockletInfo } = require('@blocklet/meta/lib/info');
6
+ const logger = require('@abtnode/logger')('@abtnode/core:storage:export');
7
+
8
+ const states = require('../../../states');
9
+ const { BlockletExport } = require('./blocklet-export');
10
+ const { BlockletExtrasExport } = require('./blocklet-extras-export');
11
+ const { BlockletsExport } = require('./blocklets-export');
12
+ const { DataBackup } = require('../backup/data');
13
+ const { RoutingRuleBackup } = require('../backup/routing-rule');
14
+ const { LogsBackup } = require('../backup/logs');
15
+ const { writeExportMeta } = require('./export-meta');
16
+ const { dockerExecChown } = require('../../../util/docker/docker-exec-chown');
17
+ const checkDockerRunHistory = require('../../../util/docker/check-docker-run-history');
18
+ const { dockerBackupPgBlockletDb } = require('../../../util/docker/docker-backup-pg-blocklet-db');
19
+
20
+ class BlockletExporter {
21
+ constructor({ appDid, outDir, serverDir, options = {}, progressCallback } = {}) {
22
+ if (isEmpty(appDid) || !isValid(appDid)) {
23
+ throw new Error(`appDid(${appDid}) is not a valid did`);
24
+ }
25
+ if (isEmpty(outDir)) {
26
+ throw new Error('outDir is required');
27
+ }
28
+
29
+ this.appDid = appDid;
30
+ this.outDir = outDir;
31
+ this._serverDir = serverDir;
32
+ this.includeLogs = options.includeLogs || false;
33
+ this.progressCallback = progressCallback || (() => {});
34
+ }
35
+
36
+ async initialize() {
37
+ this.blocklet = await states.blocklet.getBlocklet(this.appDid);
38
+ if (isEmpty(this.blocklet)) {
39
+ throw new Error(`Blocklet ${this.appDid} not found`);
40
+ }
41
+
42
+ this.serverDir = this._serverDir || process.env.ABT_NODE_DATA_DIR;
43
+ this.backupDir = this.outDir;
44
+ ensureDirSync(this.backupDir);
45
+
46
+ // Extract appSk from blocklet wallet
47
+ const nodeInfo = await states.node.read();
48
+ const { wallet } = getBlockletInfo(this.blocklet, nodeInfo.sk);
49
+ this.appSk = wallet.secretKey;
50
+ this.appDid = wallet.address;
51
+ this.serverDid = nodeInfo.did;
52
+
53
+ logger.info('export initialized', { appDid: this.appDid, outDir: this.outDir });
54
+ }
55
+
56
+ async prepareDocker() {
57
+ const nodeInfo = await states.node.read();
58
+
59
+ if (checkDockerRunHistory(nodeInfo)) {
60
+ let paths = [];
61
+ if (this.blocklet) {
62
+ paths = this.blocklet.children.map((child) => {
63
+ if (!child.meta?.docker?.image) {
64
+ return null;
65
+ }
66
+ return path.join(this.serverDir, 'blocklets', child.meta.name, child.meta.version);
67
+ });
68
+ }
69
+ await dockerExecChown({
70
+ name: `${this.blocklet.meta.did}-export`,
71
+ dirs: [path.join(this.serverDir, 'data', this.blocklet.meta.did), ...paths.filter(Boolean)],
72
+ });
73
+ }
74
+ }
75
+
76
+ async backupPostgres() {
77
+ if (!this.blocklet) {
78
+ return;
79
+ }
80
+ const dataDir =
81
+ this.blocklet.environments?.find((v) => v.key === 'BLOCKLET_APP_DATA_DIR')?.value ||
82
+ path.join(this.serverDir, 'data', this.blocklet.appPid || this.blocklet.appDid);
83
+ const dbPath = path.join(dataDir, 'blocklet.db');
84
+ await dockerBackupPgBlockletDb(dbPath);
85
+ }
86
+
87
+ async export() {
88
+ await this.initialize();
89
+ this.progressCallback('Preparing Docker environment...');
90
+
91
+ await this.prepareDocker();
92
+ await this.backupPostgres();
93
+
94
+ const input = { appDid: this.appDid };
95
+
96
+ // Data must be exported first
97
+ this.progressCallback('Exporting data directory...');
98
+ const dataBackup = new DataBackup(input);
99
+ dataBackup.ensureParams(this);
100
+ await dataBackup.export();
101
+
102
+ // Export blocklet state, extras, bundles, and routing rules in parallel
103
+ this.progressCallback('Exporting blocklet state and bundles...');
104
+ const storages = [
105
+ new BlockletExport(input),
106
+ new BlockletsExport(input),
107
+ new BlockletExtrasExport(input),
108
+ new RoutingRuleBackup(input),
109
+ ];
110
+ await Promise.all(
111
+ storages.map((storage) => {
112
+ storage.ensureParams(this);
113
+ return storage.export();
114
+ })
115
+ );
116
+
117
+ // Optional: export logs
118
+ if (this.includeLogs) {
119
+ this.progressCallback('Exporting logs...');
120
+ const logsBackup = new LogsBackup(input);
121
+ logsBackup.ensureParams(this);
122
+ await logsBackup.export();
123
+ }
124
+
125
+ // Write export metadata
126
+ this.progressCallback('Writing export metadata...');
127
+ const meta = await writeExportMeta({
128
+ outDir: this.backupDir,
129
+ blocklet: this.blocklet,
130
+ serverDid: this.serverDid,
131
+ appSk: this.appSk,
132
+ appDid: this.appDid,
133
+ });
134
+
135
+ logger.info('export completed', { appDid: this.appDid, outDir: this.outDir });
136
+ return meta;
137
+ }
138
+ }
139
+
140
+ module.exports = { BlockletExporter };
@@ -0,0 +1,110 @@
1
+ const fs = require('fs-extra');
2
+ const { join } = require('path');
3
+ const fg = require('fast-glob');
4
+ const logger = require('@abtnode/logger')('@abtnode/core:storage:import');
5
+
6
+ const { EXPORT_META_FILENAME } = require('../export/export-meta');
7
+ const { installApplicationFromBackup } = require('../../manager/helper/install-application-from-backup');
8
+ const { zipToDir } = require('../utils/zip');
9
+
10
+ class BlockletImporter {
11
+ constructor({ inputDir, manager, states, context = {}, progressCallback } = {}) {
12
+ if (!inputDir || !fs.existsSync(inputDir)) {
13
+ throw new Error(`Input directory does not exist: ${inputDir}`);
14
+ }
15
+ this.inputDir = inputDir;
16
+ this.manager = manager;
17
+ this.states = states;
18
+ this.context = context;
19
+ this.progressCallback = progressCallback || (() => {});
20
+ }
21
+
22
+ readExportMeta() {
23
+ const metaPath = join(this.inputDir, EXPORT_META_FILENAME);
24
+ if (!fs.existsSync(metaPath)) {
25
+ throw new Error(`Export metadata not found: ${metaPath}`);
26
+ }
27
+ return fs.readJSONSync(metaPath);
28
+ }
29
+
30
+ validate(meta) {
31
+ if (!meta.version) {
32
+ throw new Error('Invalid export metadata: missing version');
33
+ }
34
+ if (!meta.appSk) {
35
+ throw new Error('Invalid export metadata: missing appSk');
36
+ }
37
+ if (!meta.appDid) {
38
+ throw new Error('Invalid export metadata: missing appDid');
39
+ }
40
+
41
+ // Check required files exist
42
+ const requiredFiles = ['blocklet.json', 'blocklet-extras.json', 'routing_rule.json'];
43
+ for (const file of requiredFiles) {
44
+ if (!fs.existsSync(join(this.inputDir, file))) {
45
+ throw new Error(`Required file missing in export directory: ${file}`);
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Extract bundle zip files so installApplicationFromBackup can find them.
52
+ * Similar to BlockletsRestore.import() but keeps the zip files intact.
53
+ */
54
+ async extractBundleZips() {
55
+ const blockletsDir = join(this.inputDir, 'blocklets');
56
+ if (!fs.existsSync(blockletsDir)) {
57
+ return;
58
+ }
59
+
60
+ // Only match bundle zips at depth 1 (<name>/<version>.zip),
61
+ // not nested zips inside already-extracted bundle directories
62
+ const paths = await fg('*/*.zip', {
63
+ cwd: blockletsDir,
64
+ onlyFiles: true,
65
+ absolute: true,
66
+ });
67
+
68
+ await Promise.all(
69
+ paths.map(async (zipPath) => {
70
+ const target = zipPath.replace(/.zip$/, '');
71
+ if (!fs.existsSync(target)) {
72
+ fs.ensureDirSync(target);
73
+ }
74
+ await zipToDir(zipPath, target);
75
+ logger.info('extracted bundle zip', { zipPath, target });
76
+ })
77
+ );
78
+ }
79
+
80
+ async import() {
81
+ this.progressCallback('Reading export metadata...');
82
+ const meta = this.readExportMeta();
83
+ this.validate(meta);
84
+
85
+ logger.info('import started', {
86
+ appDid: meta.appDid,
87
+ blockletName: meta.blockletName,
88
+ sourceServer: meta.sourceServerDid,
89
+ });
90
+
91
+ this.progressCallback('Extracting bundle archives...');
92
+ await this.extractBundleZips();
93
+
94
+ this.progressCallback('Installing blocklet from export...');
95
+ const result = await installApplicationFromBackup({
96
+ url: `file://${this.inputDir}`,
97
+ appSk: meta.appSk,
98
+ moveDir: false,
99
+ manager: this.manager,
100
+ states: this.states,
101
+ sync: true,
102
+ context: { startImmediately: false, skipHooks: true, ...this.context },
103
+ });
104
+
105
+ logger.info('import completed', { appDid: meta.appDid });
106
+ return { meta, blocklet: result };
107
+ }
108
+ }
109
+
110
+ module.exports = { BlockletImporter };
@@ -134,6 +134,28 @@ module.exports = ({
134
134
  });
135
135
  }
136
136
 
137
+ // When receiving installed event from CLI (e.g. blocklet import), set up routing
138
+ // in the daemon. The router deduplicates queued changes, so this is safe even if
139
+ // handleBlockletEvent already processed routing for this blocklet.
140
+ if (name === BlockletEvents.installed) {
141
+ const blocklet = data?.blocklet || data;
142
+ if (blocklet?.meta?.did) {
143
+ ensureBlockletRouting(blocklet, data?.context || {})
144
+ .then((changed) => {
145
+ if (changed) {
146
+ return handleBlockletRouting({
147
+ did: blocklet.meta.did,
148
+ message: 'Install blocklet (eventHub)',
149
+ });
150
+ }
151
+ return null;
152
+ })
153
+ .catch((error) => {
154
+ logger.error('routing setup from eventHub failed', { event: name, did: blocklet?.meta?.did, error });
155
+ });
156
+ }
157
+ }
158
+
137
159
  if (typeof eventHandler === 'function') {
138
160
  eventHandler({ name, data });
139
161
  }
@@ -467,6 +489,11 @@ module.exports = ({
467
489
  did: blocklet.meta.did,
468
490
  message: 'Connect blocklet to DID Spaces',
469
491
  });
492
+ } else if (BlockletEvents.gatewayConfigChanged === eventName) {
493
+ await handleBlockletRouting({
494
+ did: blocklet.meta.did,
495
+ message: 'Update blocklet gateway config',
496
+ });
470
497
  } else if (BlockletEvents.backupProgress === eventName && payload?.completed) {
471
498
  try {
472
499
  const backupEndpoint = getBackupEndpoint(blocklet?.environments);
@@ -700,6 +727,7 @@ module.exports = ({
700
727
  BlockletEvents.downloadBundleProgress,
701
728
 
702
729
  BlockletEvents.spaceConnected,
730
+ BlockletEvents.gatewayConfigChanged,
703
731
  BlockletEvents.nftConsumed,
704
732
 
705
733
  BlockletEvents.configTheme,
package/lib/index.js CHANGED
@@ -413,6 +413,7 @@ function ABTNode(options) {
413
413
  backupBlocklet: blockletManager.backupBlocklet.bind(blockletManager),
414
414
  abortBlockletBackup: blockletManager.abortBlockletBackup.bind(blockletManager),
415
415
  restoreBlocklet: blockletManager.restoreBlocklet.bind(blockletManager),
416
+ importBlocklet: blockletManager.importBlocklet.bind(blockletManager),
416
417
  migrateApplicationToStructV2: blockletManager.migrateApplicationToStructV2.bind(blockletManager),
417
418
  syncAppConfig: blockletManager.syncAppConfig.bind(blockletManager),
418
419
  setBlockletBlurhash: blockletManager.setBlockletBlurhash.bind(blockletManager),
@@ -1897,12 +1897,13 @@ module.exports = function getRouterHelpers({
1897
1897
  getAllRoutingParams: async () => {
1898
1898
  try {
1899
1899
  // Parallelize all independent async operations
1900
- const [info, { sites }, services, certificates, wafDisabledList] = await Promise.all([
1900
+ const [info, { sites }, services, certificates, wafDisabledList, noIndexOverrides] = await Promise.all([
1901
1901
  nodeState._read(),
1902
1902
  readAllRoutingSites(),
1903
1903
  blockletState.getServices(),
1904
1904
  httpsEnabled ? certManager.getAllNormal() : Promise.resolve([]),
1905
1905
  states.blockletExtras.getWafDisabledBlocklets(),
1906
+ states.blockletExtras.getNoIndexOverrides(),
1906
1907
  ]);
1907
1908
 
1908
1909
  // Fetch site info for WAF disabled blocklets in parallel
@@ -1921,6 +1922,7 @@ module.exports = function getRouterHelpers({
1921
1922
  services,
1922
1923
  nodeInfo: info,
1923
1924
  wafDisabledBlocklets,
1925
+ noIndexOverrides,
1924
1926
  };
1925
1927
  } catch (err) {
1926
1928
  logger.error('Read routing rules failed', { error: err });
@@ -1939,10 +1941,11 @@ module.exports = function getRouterHelpers({
1939
1941
  getBlockletRoutingParams: async (blockletDid) => {
1940
1942
  try {
1941
1943
  // Parallelize all independent async operations
1942
- const [info, sites, certificates] = await Promise.all([
1944
+ const [info, sites, certificates, noIndexOverrides] = await Promise.all([
1943
1945
  nodeState._read(),
1944
1946
  readBlockletRoutingSite(blockletDid),
1945
1947
  httpsEnabled ? certManager.getAllNormal() : Promise.resolve([]),
1948
+ states.blockletExtras.getNoIndexOverrides(),
1946
1949
  ]);
1947
1950
 
1948
1951
  if (!sites || sites.length === 0) {
@@ -1958,6 +1961,7 @@ module.exports = function getRouterHelpers({
1958
1961
  headers: get(info, 'routing.headers', {}),
1959
1962
  nodeInfo: info,
1960
1963
  wafDisabledBlocklets: [],
1964
+ noIndexOverrides,
1961
1965
  };
1962
1966
  } catch (err) {
1963
1967
  logger.error('router: getBlockletRoutingParams failed', { blockletDid, error: err });
@@ -1997,6 +2001,7 @@ module.exports = function getRouterHelpers({
1997
2001
  headers: get(info, 'routing.headers', {}),
1998
2002
  nodeInfo: info,
1999
2003
  wafDisabledBlocklets,
2004
+ noIndexOverrides: {},
2000
2005
  };
2001
2006
  } catch (err) {
2002
2007
  logger.error('router: getSystemRoutingParams failed', { error: err });
@@ -262,6 +262,7 @@ class Router {
262
262
  services = [],
263
263
  nodeInfo = {},
264
264
  wafDisabledBlocklets = [],
265
+ noIndexOverrides = {},
265
266
  } = (await this[fn]()) || {};
266
267
 
267
268
  if (!Array.isArray(sites)) {
@@ -332,6 +333,7 @@ class Router {
332
333
  wafPolicy,
333
334
  cacheEnabled: isGatewayCacheEnabled(nodeInfo),
334
335
  wafDisabledBlocklets,
336
+ noIndexOverrides,
335
337
  enableDefaultServer: nodeInfo.routing.enableDefaultServer ?? false,
336
338
  enableIpServer: nodeInfo.routing.enableIpServer ?? false,
337
339
  };
@@ -434,13 +436,21 @@ class Router {
434
436
  // eslint-disable-next-line no-await-in-loop
435
437
  const rawParams = await this.getBlockletRoutingParams(did);
436
438
  if (rawParams && rawParams.sites && rawParams.sites.length > 0) {
437
- const { sites, certificates, headers = {}, nodeInfo = {}, wafDisabledBlocklets = [] } = rawParams;
439
+ const {
440
+ sites,
441
+ certificates,
442
+ headers = {},
443
+ nodeInfo = {},
444
+ wafDisabledBlocklets = [],
445
+ noIndexOverrides = {},
446
+ } = rawParams;
438
447
  const blockletParams = {
439
448
  routingTable: getRoutingTable({ sites, nodeInfo }),
440
449
  certificates,
441
450
  commonHeaders: headers,
442
451
  nodeInfo: pick(nodeInfo, ['did', 'name', 'version', 'port', 'mode', 'enableWelcomePage', 'routing']),
443
452
  wafDisabledBlocklets,
453
+ noIndexOverrides,
444
454
  };
445
455
 
446
456
  if (typeof this.provider.updateSingleBlocklet === 'function') {
@@ -7,6 +7,21 @@ const logger = require('@abtnode/logger')('@abtnode/core:router:security:limiter
7
7
 
8
8
  const states = require('../../states');
9
9
 
10
+ /**
11
+ * Check if an IP is a loopback or private address that should never be blocked.
12
+ * Blocking these IPs can cause the server to lock itself out.
13
+ */
14
+ function isSkippedIP(ip) {
15
+ return (
16
+ ip === '127.0.0.1' ||
17
+ ip === '::1' ||
18
+ ip === '0.0.0.0' ||
19
+ ip.startsWith('10.') ||
20
+ ip.startsWith('192.168.') ||
21
+ /^172\.(1[6-9]|2\d|3[01])\./.test(ip)
22
+ );
23
+ }
24
+
10
25
  /**
11
26
  * Create a rate limiter for suspicious requests.
12
27
  * https://github.com/animir/node-rate-limiter-flexible/wiki/Options
@@ -29,6 +44,11 @@ function createLimiter(options, onBlocked = () => {}) {
29
44
  logger.info('Rate limiter created', { options });
30
45
 
31
46
  const check = async (ip, points = 1) => {
47
+ if (isSkippedIP(ip)) {
48
+ logger.debug('Skipping private/loopback IP from rate limiting', { ip });
49
+ return 0;
50
+ }
51
+
32
52
  try {
33
53
  const result = await limiter.consume(ip, points);
34
54
  logger.debug('Rate limit not exceeded', { ip, result });
@@ -351,6 +351,18 @@ class BlockletExtrasState extends BaseState {
351
351
  getWafDisabledBlocklets() {
352
352
  return super.find({ where: { 'settings.gateway.wafPolicy.enabled': false } }, { did: 1 });
353
353
  }
354
+
355
+ async getNoIndexOverrides() {
356
+ const docs = await super.find({}, { did: 1, configs: 1 });
357
+ const overrides = {};
358
+ for (const doc of docs) {
359
+ const config = (doc.configs || []).find((c) => c.key === 'APP_NO_INDEX');
360
+ if (config && config.value !== undefined && config.value !== '') {
361
+ overrides[doc.did] = !['false', '0'].includes(String(config.value).toLowerCase());
362
+ }
363
+ }
364
+ return overrides;
365
+ }
354
366
  }
355
367
 
356
368
  module.exports = BlockletExtrasState;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.10",
6
+ "version": "1.17.11-beta-20260225-043848-68611a07",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -17,19 +17,19 @@
17
17
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
18
18
  "license": "Apache-2.0",
19
19
  "dependencies": {
20
- "@abtnode/analytics": "1.17.10",
21
- "@abtnode/auth": "1.17.10",
22
- "@abtnode/certificate-manager": "1.17.10",
23
- "@abtnode/constant": "1.17.10",
24
- "@abtnode/cron": "1.17.10",
25
- "@abtnode/db-cache": "1.17.10",
26
- "@abtnode/docker-utils": "1.17.10",
27
- "@abtnode/logger": "1.17.10",
28
- "@abtnode/models": "1.17.10",
29
- "@abtnode/queue": "1.17.10",
30
- "@abtnode/rbac": "1.17.10",
31
- "@abtnode/router-provider": "1.17.10",
32
- "@abtnode/util": "1.17.10",
20
+ "@abtnode/analytics": "1.17.11-beta-20260225-043848-68611a07",
21
+ "@abtnode/auth": "1.17.11-beta-20260225-043848-68611a07",
22
+ "@abtnode/certificate-manager": "1.17.11-beta-20260225-043848-68611a07",
23
+ "@abtnode/constant": "1.17.11-beta-20260225-043848-68611a07",
24
+ "@abtnode/cron": "1.17.11-beta-20260225-043848-68611a07",
25
+ "@abtnode/db-cache": "1.17.11-beta-20260225-043848-68611a07",
26
+ "@abtnode/docker-utils": "1.17.11-beta-20260225-043848-68611a07",
27
+ "@abtnode/logger": "1.17.11-beta-20260225-043848-68611a07",
28
+ "@abtnode/models": "1.17.11-beta-20260225-043848-68611a07",
29
+ "@abtnode/queue": "1.17.11-beta-20260225-043848-68611a07",
30
+ "@abtnode/rbac": "1.17.11-beta-20260225-043848-68611a07",
31
+ "@abtnode/router-provider": "1.17.11-beta-20260225-043848-68611a07",
32
+ "@abtnode/util": "1.17.11-beta-20260225-043848-68611a07",
33
33
  "@arcblock/did": "1.29.8",
34
34
  "@arcblock/did-connect-js": "1.29.8",
35
35
  "@arcblock/did-ext": "1.29.8",
@@ -40,15 +40,15 @@
40
40
  "@arcblock/pm2-events": "^0.0.5",
41
41
  "@arcblock/validator": "1.29.8",
42
42
  "@arcblock/vc": "1.29.8",
43
- "@blocklet/constant": "1.17.10",
43
+ "@blocklet/constant": "1.17.11-beta-20260225-043848-68611a07",
44
44
  "@blocklet/did-space-js": "^1.2.16",
45
- "@blocklet/env": "1.17.10",
45
+ "@blocklet/env": "1.17.11-beta-20260225-043848-68611a07",
46
46
  "@blocklet/error": "^0.3.5",
47
- "@blocklet/meta": "1.17.10",
48
- "@blocklet/resolver": "1.17.10",
49
- "@blocklet/sdk": "1.17.10",
50
- "@blocklet/server-js": "1.17.10",
51
- "@blocklet/store": "1.17.10",
47
+ "@blocklet/meta": "1.17.11-beta-20260225-043848-68611a07",
48
+ "@blocklet/resolver": "1.17.11-beta-20260225-043848-68611a07",
49
+ "@blocklet/sdk": "1.17.11-beta-20260225-043848-68611a07",
50
+ "@blocklet/server-js": "1.17.11-beta-20260225-043848-68611a07",
51
+ "@blocklet/store": "1.17.11-beta-20260225-043848-68611a07",
52
52
  "@blocklet/theme": "^3.5.2",
53
53
  "@fidm/x509": "^1.2.1",
54
54
  "@ocap/mcrypto": "1.29.8",
@@ -113,5 +113,5 @@
113
113
  "express": "^4.18.2",
114
114
  "unzipper": "^0.10.11"
115
115
  },
116
- "gitHead": "8a940042dc8376648117826d777254d99628a61b"
116
+ "gitHead": "984a5f485c38205d8240b667cca26ddad0bbf6ac"
117
117
  }