fuse-core-express 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/decrypt-loader.mjs +301 -0
- package/index.mjs +218 -0
- package/package.json +76 -0
- package/private-1.0.2.pem +28 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# FuseCore Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Redis TLS Support**: Added `enableTls` parameter to Redis socket configuration
|
|
7
|
+
- New `enableTls` option in Redis configuration for secure connections
|
|
8
|
+
- Default value is `false` for backward compatibility
|
|
9
|
+
- Supports both standard Redis connections and TLS-enabled connections
|
|
10
|
+
- Updated documentation and examples to include TLS configuration
|
|
11
|
+
- **Redis Configuration Cleanup**: Removed unsupported parameters from Redis configuration
|
|
12
|
+
- Removed `connectTimeout` and `commandTimeout` parameters (not in official docs)
|
|
13
|
+
- Kept only officially supported socket parameters: `tls`, `reconnectStrategy`
|
|
14
|
+
- Configuration now strictly follows Redis official documentation
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Build Process**: Separated version bumping from crypto build process
|
|
18
|
+
- Version increment and key generation moved to `npm run version:bump`
|
|
19
|
+
- Crypto build now uses existing keys instead of generating new ones
|
|
20
|
+
- New keys and version changes are automatically committed to git
|
|
21
|
+
- Added git tag creation for version tracking
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Version & Keys Management**: New `build/version-keys.mjs` script for version and key management
|
|
25
|
+
- Automatic version increment with SemVer compliance
|
|
26
|
+
- Git integration for committing keys and version changes
|
|
27
|
+
- Git tag creation for release tracking
|
|
28
|
+
- Clean working directory validation
|
|
29
|
+
- **MIT License Compliance**: Private keys now included in published packages
|
|
30
|
+
- Private key automatically copied to `dist-encrypted` directory
|
|
31
|
+
- Package `files` field updated to include private key
|
|
32
|
+
- README updated with MIT compliance notices
|
|
33
|
+
- Users can now use the package without external key management
|
|
34
|
+
- **MIT License Documentation**: Added complete MIT license documentation
|
|
35
|
+
- Created `LICENSE` file with full MIT license text
|
|
36
|
+
- Added MIT license badge to README header
|
|
37
|
+
- Added complete MIT license text to README
|
|
38
|
+
- Updated package `files` field to include LICENSE file
|
|
39
|
+
- **Package Name Update**: Changed npm package name from `fuse-core` to `fusecore` to `fuse-core-express`
|
|
40
|
+
- Updated package.json name field
|
|
41
|
+
- Updated all README examples to use new package name
|
|
42
|
+
- Updated encrypted version documentation
|
|
43
|
+
- **Encrypted Version Path Fix**: Fixed module path resolution in encrypted version
|
|
44
|
+
- Fixed decrypt loader to correctly resolve `.fec` file paths
|
|
45
|
+
- Updated path resolution to include `cjs/` directory prefix
|
|
46
|
+
- Resolved ENOENT errors when loading encrypted modules
|
|
47
|
+
- **ESM/CJS Compatibility**: Enhanced encrypted version to support both module systems
|
|
48
|
+
- Added intelligent path resolution for ESM and CJS directories
|
|
49
|
+
- Prioritizes ESM modules when available (ESM-first approach)
|
|
50
|
+
- Falls back to CJS modules when ESM not available
|
|
51
|
+
- Maintains backward compatibility with existing installations
|
|
52
|
+
- **ESM Runtime Compatibility**: Fixed `require is not defined` error in ESM environments
|
|
53
|
+
- Implemented dual-mode module loading system for CommonJS and ESM
|
|
54
|
+
- Added async/await support for ESM module execution
|
|
55
|
+
- Updated decrypt loader to handle both runtime environments seamlessly
|
|
56
|
+
- Fixed module execution context for encrypted code in ESM mode
|
|
57
|
+
- **Global Singleton Pattern**: Fixed ESM module import inconsistency issues
|
|
58
|
+
- Implemented global singleton using Symbol.for() to ensure single instance across all imports
|
|
59
|
+
- Fixed issue where multiple imports of FuseCore created different instances
|
|
60
|
+
- Ensured consistent state across app.mjs and route1.mjs imports
|
|
61
|
+
- Added proper state management for initialization status across module boundaries
|
|
62
|
+
- **Proxy Architecture**: Redesigned encrypted version to proxy to original API
|
|
63
|
+
- Generated index.mjs now only handles decryption logic
|
|
64
|
+
- After decryption, all API calls are proxied to the original source code
|
|
65
|
+
- Maintains complete compatibility with original FuseCore API
|
|
66
|
+
- Simplified architecture: decrypt once, then use original functionality
|
|
67
|
+
- Eliminates need to reimplement API logic in encrypted version
|
|
68
|
+
|
|
69
|
+
## [1.1.0] - 2025-07-31
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **Redis Cache Support**: FuseCore now supports Redis as a cache backend
|
|
73
|
+
- Full Redis integration with connection management
|
|
74
|
+
- Support for Redis-specific operations: `exists`, `expire`, `ttl`, `keys`, `flushAll`
|
|
75
|
+
- Configurable connection options including timeout and client settings
|
|
76
|
+
- Automatic reconnection and error handling
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
- **Redis Cache Type Preservation**: Fixed critical bug where JSON strings were incorrectly parsed as objects
|
|
80
|
+
- JSON strings now maintain their string type when stored and retrieved
|
|
81
|
+
- Objects are properly serialized/deserialized
|
|
82
|
+
- Backward compatibility with existing cached data
|
|
83
|
+
|
|
84
|
+
### Removed
|
|
85
|
+
- **Nestia-specific UA Properties**: Removed Nestia-related properties from getUAInfo function
|
|
86
|
+
- Removed: `isNestiaServer`, `isNestiaClient`, `isNestiaAgentClient`, `nestiaClientId`
|
|
87
|
+
- Removed: `isNestiaBusClient`, `isNestiaAppClient`, `isNestiaLotteryClient`, `isNestiaMalaysiaLotteryClient`
|
|
88
|
+
- Cleaned up function implementation and documentation
|
|
89
|
+
- Updated README documentation to reflect changes
|
|
90
|
+
- Fixed code quality warnings (assignment in conditions, unused variables, logic errors)
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- **Cache API**: All cache operations are now asynchronous (`async/await`)
|
|
94
|
+
- Memory cache methods updated to return Promises for consistency
|
|
95
|
+
- Tests updated to use async/await pattern
|
|
96
|
+
|
|
97
|
+
### Configuration
|
|
98
|
+
New Redis cache configuration options:
|
|
99
|
+
```javascript
|
|
100
|
+
{
|
|
101
|
+
cache: {
|
|
102
|
+
impl: 'redis', // 'memory' or 'redis'
|
|
103
|
+
redis: {
|
|
104
|
+
url: 'redis://localhost:6379',
|
|
105
|
+
enableTls: false, // Enable TLS/SSL connection
|
|
106
|
+
clientOptions: {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Migration Guide
|
|
113
|
+
- Update cache usage to use `await` for all cache operations
|
|
114
|
+
- No breaking changes for existing memory cache usage
|
|
115
|
+
- Redis cache requires `redis` npm package (automatically installed)
|
|
116
|
+
|
|
117
|
+
## [1.0.0] - 2025-07-31
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
- Initial release
|
|
121
|
+
- Migrated from nestia-web to FuseCore
|
|
122
|
+
- Core modules: Ajax, Cache (memory), Logger, Manifest, Monitor
|
|
123
|
+
- Complete test suite
|
|
124
|
+
- Documentation and examples
|
|
125
|
+
|
|
126
|
+
### Removed
|
|
127
|
+
- Config module (deprecated)
|
|
128
|
+
- Internal network dependencies replaced with public APIs
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ds.3783
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
# FuseCore - Express.js Development Toolkit
|
|
2
|
+
|
|
3
|
+
> 🔐 **This package contains encrypted and obfuscated code**
|
|
4
|
+
>
|
|
5
|
+
> **Version:** 1.0.2
|
|
6
|
+
> **Protection:** JavaScript Obfuscation + AES-256-CBC + RSA-2048
|
|
7
|
+
> **MIT Compliant:** Private key included in package for open source compliance
|
|
8
|
+
|
|
9
|
+
## ⚠️ Security Notice
|
|
10
|
+
|
|
11
|
+
This package contains encrypted code that is protected by multiple layers of security. The private key is included in the package to comply with MIT open source license requirements. The code is protected by:
|
|
12
|
+
|
|
13
|
+
- **JavaScript Obfuscation** - Code structure completely obfuscated
|
|
14
|
+
- **AES-256-CBC Encryption** - All source code encrypted
|
|
15
|
+
- **RSA-2048 Key Protection** - AES keys encrypted with RSA
|
|
16
|
+
- **Digital Signature** - Code integrity verification
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import FuseCore from 'fuse-core-express';
|
|
22
|
+
|
|
23
|
+
// Initialize decryption with included private key (MIT compliant)
|
|
24
|
+
FuseCore.initDecryption('./private-1.0.2.pem');
|
|
25
|
+
|
|
26
|
+
// Then use normally
|
|
27
|
+
await FuseCore.init();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
**FuseCore** - A comprehensive Express.js toolkit that provides essential web development utilities including caching, logging, monitoring, AJAX handling, and more. Built with MIT open source license.
|
|
36
|
+
|
|
37
|
+
### init options
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
common: {
|
|
43
|
+
expressApp: null,
|
|
44
|
+
listenHostname: '',
|
|
45
|
+
listenPort: 3000,
|
|
46
|
+
},
|
|
47
|
+
cache: {
|
|
48
|
+
impl: 'memory', // 'memory' or 'redis'
|
|
49
|
+
redis: {
|
|
50
|
+
url: 'redis://localhost:6379',
|
|
51
|
+
connectTimeout: 10000,
|
|
52
|
+
commandTimeout: 5000,
|
|
53
|
+
enableTls: false,
|
|
54
|
+
clientOptions: {}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
log: {
|
|
58
|
+
path: '',
|
|
59
|
+
level: '',
|
|
60
|
+
extraZipStreams: [],
|
|
61
|
+
takeOverConsole: false,
|
|
62
|
+
extraOutputStreams: {
|
|
63
|
+
info: process.stdout,
|
|
64
|
+
error: process.stderr,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
ajax: {
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
slowThreshold: 3000,
|
|
70
|
+
defaultHeaders: {
|
|
71
|
+
"X-Requested-With": "FuseCore Web component V1.0"
|
|
72
|
+
},
|
|
73
|
+
agentOptions:{}
|
|
74
|
+
},
|
|
75
|
+
manifest: {
|
|
76
|
+
path: '',
|
|
77
|
+
name: '',
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
monitor: {
|
|
81
|
+
prefix: '',
|
|
82
|
+
suffix: '',
|
|
83
|
+
mem: true,
|
|
84
|
+
cpu: true,
|
|
85
|
+
req404: true,
|
|
86
|
+
req5xx: true,
|
|
87
|
+
},
|
|
88
|
+
isPrimaryProcess: cluster.isPrimary || cluster.isMaster
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
* common.expressApp (object) optional,Express.app object.
|
|
94
|
+
* common.listenHostname (string) optional, hostname or ip http server listens on, used for config server callback.
|
|
95
|
+
* common.listenPort (number) optional,port number http server listens on, used for config server callback.
|
|
96
|
+
* cache.impl (string) optional, cache implementation, 'memory' or 'redis', default is memory.
|
|
97
|
+
* cache.redis.url (string) optional, Redis connection URL, default is 'redis://localhost:6379'.
|
|
98
|
+
|
|
99
|
+
* cache.redis.enableTls (boolean) optional, enable TLS/SSL for Redis connection, default is false.
|
|
100
|
+
* cache.redis.clientOptions (object) optional, additional Redis client options.
|
|
101
|
+
* log.path (string) optional, directory to save log files.
|
|
102
|
+
* log.level (string) optional, minimal level written to log file,default is info.
|
|
103
|
+
* log.extraZipStreams (array) optional, streams for access log.
|
|
104
|
+
* log.takeOverConsole (bool) optional, default false, indicate log module take over system console, thus all console message will be pipe to log file.
|
|
105
|
+
* log.extraOutputStreams (Object) optional, default null, extra log output stream, should be {info:[...Writable],error:[...Writable]}.
|
|
106
|
+
* ajax.timeout (number) optional, default timeout in millisecond, default is 10000.
|
|
107
|
+
* ajax.slowThreshold (number) optional, default slow log time limit in millisecond, all backend response time greater than limit will be logged to slow log file, default value is 50.
|
|
108
|
+
* ajax.defaultHeaders (object) optional, default headers send to backend, default value is {'x-requested-with':'FuseCore Web Server 1.0'}.
|
|
109
|
+
* ajax.agentOptions (object) optional, extra agent options will pass to http/https agent, also will applied to globalAgent.
|
|
110
|
+
* manifest.path (string) optional, directory contain manifest file.
|
|
111
|
+
* manifest.name (string) optional, manifest name to load.
|
|
112
|
+
|
|
113
|
+
* monitor.prefix (string) optional, prefix of all monitor keys.
|
|
114
|
+
* monitor.suffix (string) optional, suffix of all monitor keys.
|
|
115
|
+
* monitor.mem (bool) optional, auto monitor memory usage, default is false.
|
|
116
|
+
* monitor.cpu (bool) optional, auto monitor cpu usage, default is false.
|
|
117
|
+
* monitor.req404 (bool) optional, monitor 404 requests, default is false.
|
|
118
|
+
* monitor.req5xx (bool) optional, monitor 5xx requests, default is false.
|
|
119
|
+
* isPrimaryProcess (bool) optional, indicate current process is primary in cluster mode, monitor module will act differently when run as a worker process.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Monitor
|
|
124
|
+
|
|
125
|
+
### Usage:
|
|
126
|
+
|
|
127
|
+
#### Init:
|
|
128
|
+
@deprecated
|
|
129
|
+
|
|
130
|
+
var FuseCore = require('fuse-core-express');
|
|
131
|
+
//this line must @ top ,before any routes or app filters.
|
|
132
|
+
app.use(FuseCore.requestFilter());
|
|
133
|
+
|
|
134
|
+
.....
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
var Monitor = FuseCore.monitor;
|
|
138
|
+
Monitor.init(configObj);
|
|
139
|
+
|
|
140
|
+
#### Monitor Config:
|
|
141
|
+
{
|
|
142
|
+
app: Express.app object (obj,required)
|
|
143
|
+
prefix: monitor default prefix (string,optional)
|
|
144
|
+
suffix: monitor default suffix (string,optional)
|
|
145
|
+
mem: monitor memory usage (boolean,optional)
|
|
146
|
+
cpu: monitor cpu usage (boolean,optional)
|
|
147
|
+
mon404: monitor 404 response (boolean,optional)
|
|
148
|
+
mon5xx: monitor 5xx response (boolean,optional)
|
|
149
|
+
monitorPath: the path for express which remove graph drawer used to get monitor indicators and values
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#### Request filter
|
|
154
|
+
1. Used to adept nestia ip rule,when using request.ip property.
|
|
155
|
+
1. add req.realUrl property presents the url send to nginx.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
#### Request Utils
|
|
159
|
+
Provides function to resolve request headers,such as accept-language or user-agent.
|
|
160
|
+
|
|
161
|
+
##### getUAInfo
|
|
162
|
+
|
|
163
|
+
param:
|
|
164
|
+
|
|
165
|
+
* req: express request object
|
|
166
|
+
|
|
167
|
+
return:
|
|
168
|
+
```
|
|
169
|
+
{
|
|
170
|
+
isBot: false,
|
|
171
|
+
isWinPhone: false,
|
|
172
|
+
isIPhone: false,
|
|
173
|
+
isIPad: false,
|
|
174
|
+
isAndroid: false,
|
|
175
|
+
isAndroidTablet: false,
|
|
176
|
+
isTablet: false,
|
|
177
|
+
isOtherMobile: false,
|
|
178
|
+
isMobile: false,
|
|
179
|
+
isWechatMiniProg: false,
|
|
180
|
+
platform: "windows" | "ios" | "android" | "linux" | "macos" | "whatsapp" | "compatible" | "unknown",
|
|
181
|
+
platformVersion: "10.3.1",
|
|
182
|
+
browser: "msie" | "opera" | "firefox" | "chrome" | "facebook" | "weixin" | "safari" | "unknown",
|
|
183
|
+
browserVersion: "10.3.1"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
##### getLangInfo
|
|
188
|
+
|
|
189
|
+
param:
|
|
190
|
+
|
|
191
|
+
* req: express request object
|
|
192
|
+
* is4PC: true means client is requesting a web page for PC not mobile device.
|
|
193
|
+
|
|
194
|
+
return:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
{
|
|
198
|
+
languages: {
|
|
199
|
+
"zh-CN": "0,8",
|
|
200
|
+
"en": "0,6"
|
|
201
|
+
},
|
|
202
|
+
primaryLanguage: ["zh-CN"],
|
|
203
|
+
isEnglish: false,
|
|
204
|
+
lang: "zh-cn" | "en"
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
#### Log
|
|
210
|
+
|
|
211
|
+
1. provide an logger by using getLogger
|
|
212
|
+
1. automatically zip logs
|
|
213
|
+
1. automatically removes oldlogs
|
|
214
|
+
1. Requires init
|
|
215
|
+
1. FuseCore.logger's method can be called at any time, but it won't output any data until init finished.
|
|
216
|
+
|
|
217
|
+
init options:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
{
|
|
221
|
+
dir:path.join(__dirname,'/../logs'),
|
|
222
|
+
streams:FileStreamRotator.getStream({...}) | [FileStreamRotator.getStream({...})]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Usage Example:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
FuseCore.logger.info('some text',someObject);
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Manifest
|
|
234
|
+
```
|
|
235
|
+
let FuseCore=require('fuse-core-express');
|
|
236
|
+
let manifest=FuseCore.manifest;
|
|
237
|
+
|
|
238
|
+
let value=manifest.get('prop1.prop2.prop3');
|
|
239
|
+
......
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Ajax
|
|
243
|
+
|
|
244
|
+
Ajax API
|
|
245
|
+
|
|
246
|
+
###### Request Options
|
|
247
|
+
|
|
248
|
+
demo:
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
server:'lottery',
|
|
253
|
+
version:'v5.0'
|
|
254
|
+
path:'/toto/broadcast'
|
|
255
|
+
data:{
|
|
256
|
+
key1:1234,
|
|
257
|
+
key2:5678
|
|
258
|
+
},
|
|
259
|
+
method:'POST',
|
|
260
|
+
timeout:800,
|
|
261
|
+
reqContentType:'form',
|
|
262
|
+
resContentType:'json',
|
|
263
|
+
headers:{
|
|
264
|
+
SomeUserDefinedHeader:'this will pass to server'
|
|
265
|
+
},
|
|
266
|
+
isWeb:true,
|
|
267
|
+
anonymous:true,
|
|
268
|
+
cname:'toto_broadcast',
|
|
269
|
+
passClientIP:false,
|
|
270
|
+
req:req,
|
|
271
|
+
res:res
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
* server: (string) (required) Server code defined in manifest.
|
|
277
|
+
* version: (string) (optional) Version to replace '${version}' part in url.
|
|
278
|
+
* path: (string) (required) Api path apart from url defined in manifest.
|
|
279
|
+
* data: (object) (optional) Data will send to server.It should be a simple object.
|
|
280
|
+
* method: (string) (optional) Http method,default is 'GET'.
|
|
281
|
+
* timeout: (number) (optional) Timeout (microseconds) before server complete response,default is defined in config or init option DEFAULT_TIMEOUT.
|
|
282
|
+
* reqContentType (deprecated alias dataType): (string) (optional) request data format,only support 'json' and 'form'(default).
|
|
283
|
+
* resContentType (deprecated alias contentType): (string) (optional) Content format.If contentType set 'json' or server response with header 'content-type:application/json', response body will be decode automatically.
|
|
284
|
+
* headers: (object) (optional) Headers passed to server.Note:if req object is set,most of req.headers' property will passed to backend,no need to redefine headers in this option.
|
|
285
|
+
* isWeb: (bool) (optional) If true, and exists cookie named 'N1',then cookie N1's value will replace default Accept-Language value.Default is false.
|
|
286
|
+
* anonymous: (bool) (optional) If false, and exists cookie named 'token',then a header named 'Authorization' will be set with value of cookie 'token'.
|
|
287
|
+
* noCache: (bool) (optional) If true, headers named 'If-Modified-Since','If-None-Match' will be removed from header ,and 'Cache-Control' will be set 'no-cache'. Default is true.
|
|
288
|
+
* passClientIP: (bool) (optional) If true, headers ('X-Forwarded-For','X-Real-IP') will be passed to server. Default is true.
|
|
289
|
+
* req: (obj) (optional) (*required by proxy) By default,most of headers in req will pass to backend request.In proxy method,request's body stream will piped to backend request.
|
|
290
|
+
* res: (obj) (unnecessary)(*required by proxy) Proxy will handle response,when reject happened.Only if ret.status === 0 ,proxy don't handle response,you should end response, such as res.status(500).end() .
|
|
291
|
+
|
|
292
|
+
###### Response Data (also as reject error)
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
ok: false,
|
|
298
|
+
status: 0,
|
|
299
|
+
message: '',
|
|
300
|
+
error: null,
|
|
301
|
+
data: {},
|
|
302
|
+
raw: null,
|
|
303
|
+
headers: null,
|
|
304
|
+
totalCount: null,
|
|
305
|
+
duration: [0, 123000]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
* ok: (bool) indicates request is successful.
|
|
311
|
+
* status: (number) http response code from backend server.
|
|
312
|
+
* message: (string) error message when exception or error happened.
|
|
313
|
+
* error: (Error) Error object,if exists.
|
|
314
|
+
* data: (*) parsed server response content.Object if content-type option set "json",string if content-type set something else.
|
|
315
|
+
* raw: (string) original server response text,null when calling proxy method.
|
|
316
|
+
* headers: (object) response headers.
|
|
317
|
+
* duration (array) \[seconds,nanoseconds\], time used from request start to finish.
|
|
318
|
+
* totalCount: (number) same value of response's headers\["X-Total-Count"\],null if header not exists.
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
##### Ajax.request
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
FuseCore.ajax.request({
|
|
325
|
+
server: 'property',
|
|
326
|
+
version: 'v4.6',
|
|
327
|
+
timeout: 3000,
|
|
328
|
+
path: path,
|
|
329
|
+
method: 'POST',
|
|
330
|
+
isWeb:true,
|
|
331
|
+
req: req,
|
|
332
|
+
performance: false,
|
|
333
|
+
proxy: 'http://someuser:password@127.0.0.1:7666',
|
|
334
|
+
headers: {
|
|
335
|
+
'origin': 'https://property-staging.nestia.com'
|
|
336
|
+
}
|
|
337
|
+
}).then((data) => {
|
|
338
|
+
res.render('somepage',data.data);
|
|
339
|
+
}, (err) => {
|
|
340
|
+
req.app.locals.logger.error('error request backend API:' + err.message, err);
|
|
341
|
+
if (err.status) {
|
|
342
|
+
res.status(err.status).end();
|
|
343
|
+
}else{
|
|
344
|
+
res.status(500).end();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
##### Ajax.proxy
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
FuseCore.ajax.proxy({
|
|
353
|
+
server: 'property',
|
|
354
|
+
path: path,
|
|
355
|
+
method: 'POST',
|
|
356
|
+
isWeb:true,
|
|
357
|
+
req: req,
|
|
358
|
+
res: res,
|
|
359
|
+
headers: {
|
|
360
|
+
'origin': 'https://property-staging.nestia.com'
|
|
361
|
+
}
|
|
362
|
+
}).then((data) => {
|
|
363
|
+
}, (err) => {
|
|
364
|
+
FuseCore.logger.error('error upload property image:' + err.message, err);
|
|
365
|
+
if (!err.status) {
|
|
366
|
+
res.status(500).end();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
##### Ajax.requestAll
|
|
372
|
+
|
|
373
|
+
* This method is a little like Promise.all
|
|
374
|
+
* When one of requests fails,you will always get a resolve callback, which is different from Promise.all.
|
|
375
|
+
* You can use data\[n\].ok to check whether request fails, and also you can get status, and raw data if exists.
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
FuseCore.ajax.request([
|
|
379
|
+
{
|
|
380
|
+
server: 'property',
|
|
381
|
+
version: 'v4.6',
|
|
382
|
+
timeout: 3000,
|
|
383
|
+
path: '/nearby'
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
server: 'news',
|
|
387
|
+
version: 'v4.8',
|
|
388
|
+
path: '/news'
|
|
389
|
+
}
|
|
390
|
+
]).then((datas) => {
|
|
391
|
+
let propertyData=datas[0];
|
|
392
|
+
let newsData=datas[1];
|
|
393
|
+
|
|
394
|
+
//assume property backend never fails.
|
|
395
|
+
if(!newsData.ok){
|
|
396
|
+
//news api fails
|
|
397
|
+
res.render('onlyProperty',propertyData.data);
|
|
398
|
+
}else{
|
|
399
|
+
res.render('fullContent',{property:propertyData.data,news:newsData.data});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
#### Cache
|
|
405
|
+
|
|
406
|
+
FuseCore supports two cache implementations: memory cache (memory) and Redis cache (redis).
|
|
407
|
+
|
|
408
|
+
##### Memory Cache (Default)
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
let FuseCore = require('fuse-core-express');
|
|
412
|
+
|
|
413
|
+
// Initialize with memory cache
|
|
414
|
+
await FuseCore.init({
|
|
415
|
+
cache: {
|
|
416
|
+
impl: 'memory'
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
let cache = FuseCore.cache;
|
|
421
|
+
// Set a cache
|
|
422
|
+
// timeout is numeric represents seconds, default value is 300 seconds.
|
|
423
|
+
// when timeout is negative number, the cache record will never expire.
|
|
424
|
+
await cache.set('propName', 'propValue', 30);
|
|
425
|
+
|
|
426
|
+
// Get value
|
|
427
|
+
let val = await cache.get('propName');
|
|
428
|
+
console.log(val); // 'propValue'
|
|
429
|
+
|
|
430
|
+
// Memory cache cleanup has 60 seconds deviation
|
|
431
|
+
setTimeout(async function(){
|
|
432
|
+
let val = await cache.get('propName');
|
|
433
|
+
console.log(val); // should be null
|
|
434
|
+
}, 30 * 1000 + 60000);
|
|
435
|
+
|
|
436
|
+
// Setting null value deletes the key
|
|
437
|
+
await cache.set('anotherPropName', 'anotherPropValue', 30);
|
|
438
|
+
await cache.set('anotherPropName', null, 30); // This deletes the key
|
|
439
|
+
|
|
440
|
+
let val = await cache.get('anotherPropName');
|
|
441
|
+
console.log(val); // null
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
##### Redis Cache
|
|
445
|
+
|
|
446
|
+
Use Redis as cache storage, supporting distributed caching and persistence.
|
|
447
|
+
|
|
448
|
+
**Configure Redis Cache:**
|
|
449
|
+
|
|
450
|
+
```javascript
|
|
451
|
+
let FuseCore = require('fuse-core-express');
|
|
452
|
+
|
|
453
|
+
// Initialize with Redis cache
|
|
454
|
+
await FuseCore.init({
|
|
455
|
+
cache: {
|
|
456
|
+
impl: 'redis',
|
|
457
|
+
redis: {
|
|
458
|
+
url: 'redis://localhost:6379', // Redis connection URL
|
|
459
|
+
enableTls: false, // Enable TLS/SSL connection
|
|
460
|
+
clientOptions: { // Additional Redis client options
|
|
461
|
+
// Optional configuration, such as password, database, etc.
|
|
462
|
+
// password: 'your-password',
|
|
463
|
+
// database: 0
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Example with TLS enabled
|
|
470
|
+
await FuseCore.init({
|
|
471
|
+
cache: {
|
|
472
|
+
impl: 'redis',
|
|
473
|
+
redis: {
|
|
474
|
+
url: 'redis://redis-server:6380', // Redis server URL
|
|
475
|
+
enableTls: true, // Enable TLS/SSL connection
|
|
476
|
+
clientOptions: {
|
|
477
|
+
// Additional TLS options if needed
|
|
478
|
+
// password: 'your-password',
|
|
479
|
+
// database: 0
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Redis Connection URL Format:**
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
redis://[:password@]host[:port][/database]
|
|
490
|
+
redis://localhost:6379 // Local Redis, default port
|
|
491
|
+
redis://:mypassword@localhost:6379/1 // With password, using database 1
|
|
492
|
+
redis://redis-server:6379 // Remote Redis server
|
|
493
|
+
rediss://ssl-redis-server:6380 // SSL encrypted connection
|
|
494
|
+
redis://tls-redis-server:6380 // TLS enabled connection (with enableTls: true)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Basic Usage:**
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
let cache = FuseCore.cache;
|
|
501
|
+
|
|
502
|
+
// Set cache (asynchronous operation)
|
|
503
|
+
await cache.set('user:123', { name: 'John', age: 30 }, 3600); // Expires in 1 hour
|
|
504
|
+
await cache.set('session:abc', 'session-data', 0); // Never expires
|
|
505
|
+
|
|
506
|
+
// Get cache
|
|
507
|
+
let user = await cache.get('user:123');
|
|
508
|
+
console.log(user); // { name: 'John', age: 30 }
|
|
509
|
+
|
|
510
|
+
// Important: Type preservation (JSON strings are not automatically parsed into objects)
|
|
511
|
+
await cache.set('json-string', '{"name":"John"}'); // Store JSON string
|
|
512
|
+
let jsonStr = await cache.get('json-string'); // Retrieved is still a string
|
|
513
|
+
console.log(typeof jsonStr); // "string"
|
|
514
|
+
|
|
515
|
+
// Delete cache
|
|
516
|
+
await cache.set('user:123', null); // Delete key
|
|
517
|
+
|
|
518
|
+
// Check if key exists (Redis-specific method)
|
|
519
|
+
let exists = await cache.exists('user:123');
|
|
520
|
+
console.log(exists); // false
|
|
521
|
+
|
|
522
|
+
// Set expiration time (Redis-specific method)
|
|
523
|
+
await cache.set('temp:data', 'some-value');
|
|
524
|
+
await cache.expire('temp:data', 600); // Expires in 10 minutes
|
|
525
|
+
|
|
526
|
+
// Get TTL (Redis-specific method)
|
|
527
|
+
let ttl = await cache.ttl('temp:data');
|
|
528
|
+
console.log(ttl); // Remaining seconds
|
|
529
|
+
|
|
530
|
+
// Get matching keys (Redis-specific method)
|
|
531
|
+
let keys = await cache.keys('user:*');
|
|
532
|
+
console.log(keys); // ['user:123', 'user:456', ...]
|
|
533
|
+
|
|
534
|
+
// Clear all cache (Redis-specific method)
|
|
535
|
+
await cache.flushAll();
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Error Handling:**
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
try {
|
|
542
|
+
await cache.set('mykey', 'myvalue');
|
|
543
|
+
let value = await cache.get('mykey');
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('Cache operation failed:', error);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Close Connection:**
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
// FuseCore will automatically close Redis connection when the application shuts down
|
|
553
|
+
await FuseCore.shutdown();
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 📄 License
|
|
559
|
+
|
|
560
|
+
**FuseCore** follows the [MIT License](LICENSE) open source license.
|
|
561
|
+
|
|
562
|
+
### MIT License
|
|
563
|
+
|
|
564
|
+
Copyright (c) 2024 Ds.3783
|
|
565
|
+
|
|
566
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
567
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
568
|
+
in the Software without restriction, including without limitation the rights
|
|
569
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
570
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
571
|
+
furnished to do so, subject to the following conditions:
|
|
572
|
+
|
|
573
|
+
The above copyright notice and this permission notice shall be included in all
|
|
574
|
+
copies or substantial portions of the Software.
|
|
575
|
+
|
|
576
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
577
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
578
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
579
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
580
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
581
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
582
|
+
SOFTWARE.
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FuseCore Decrypt Loader (Bundle Version)
|
|
3
|
+
* 运行时束解密加载器 - 解密并加载文件束到内存
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { createGunzip } from "zlib";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
export class FuseCoreDecryptLoader {
|
|
15
|
+
constructor(privateKeyPath) {
|
|
16
|
+
this.privateKey = this.loadPrivateKey(privateKeyPath);
|
|
17
|
+
this.memoryCache = new Map(); // 内存文件缓存
|
|
18
|
+
this.bundleLoaded = false;
|
|
19
|
+
this.bundlePath = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loadPrivateKey(privateKeyPath) {
|
|
23
|
+
if (!fs.existsSync(privateKeyPath)) {
|
|
24
|
+
throw new Error(`Private key not found: ${privateKeyPath}`);
|
|
25
|
+
}
|
|
26
|
+
return fs.readFileSync(privateKeyPath, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
decryptAESKey(encryptedAESKey) {
|
|
30
|
+
const encryptedBuffer = Buffer.from(encryptedAESKey, "base64");
|
|
31
|
+
const decryptedAESKey = crypto.privateDecrypt({
|
|
32
|
+
key: this.privateKey,
|
|
33
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
34
|
+
oaepHash: "sha256"
|
|
35
|
+
}, encryptedBuffer);
|
|
36
|
+
return decryptedAESKey;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
decryptData(encryptedData, aesKey) {
|
|
40
|
+
const { algorithm, iv, data } = encryptedData;
|
|
41
|
+
const decipher = crypto.createDecipheriv(algorithm, aesKey, Buffer.from(iv, "hex"));
|
|
42
|
+
let decryptedBuffer = Buffer.concat([
|
|
43
|
+
decipher.update(Buffer.from(data, "hex")),
|
|
44
|
+
decipher.final()
|
|
45
|
+
]);
|
|
46
|
+
return decryptedBuffer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async decompressData(compressedData) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const gunzip = createGunzip();
|
|
52
|
+
const chunks = [];
|
|
53
|
+
|
|
54
|
+
gunzip.on("data", chunk => chunks.push(chunk));
|
|
55
|
+
gunzip.on("end", () => resolve(Buffer.concat(chunks)));
|
|
56
|
+
gunzip.on("error", reject);
|
|
57
|
+
|
|
58
|
+
gunzip.write(compressedData);
|
|
59
|
+
gunzip.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
verifySignature(data, signature) {
|
|
64
|
+
return true; // 简化实现
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async loadBundle(bundlePath) {
|
|
68
|
+
if (this.bundleLoaded && this.bundlePath === bundlePath) {
|
|
69
|
+
return; // 已加载同一个束
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`🔓 Loading encrypted bundle: ${bundlePath}`);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const encryptedPackage = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
|
|
76
|
+
|
|
77
|
+
if (encryptedPackage.type !== "bundle") {
|
|
78
|
+
throw new Error("Not a bundle file");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!this.verifySignature(encryptedPackage.encryptedData.data, encryptedPackage.signature)) {
|
|
82
|
+
throw new Error("Invalid signature");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`📊 Bundle info: ${encryptedPackage.fileCount} files, compressed from ${encryptedPackage.metadata.originalSize} to ${encryptedPackage.metadata.compressedSize} bytes`);
|
|
86
|
+
|
|
87
|
+
// 解密AES密钥
|
|
88
|
+
const aesKey = this.decryptAESKey(encryptedPackage.encryptedAESKey);
|
|
89
|
+
|
|
90
|
+
// 解密压缩数据
|
|
91
|
+
const decryptedCompressed = this.decryptData(encryptedPackage.encryptedData, aesKey);
|
|
92
|
+
|
|
93
|
+
// 解压缩数据
|
|
94
|
+
const decompressedBuffer = await this.decompressData(decryptedCompressed);
|
|
95
|
+
const bundleData = JSON.parse(decompressedBuffer.toString("utf8"));
|
|
96
|
+
|
|
97
|
+
// 将所有文件加载到内存缓存
|
|
98
|
+
this.memoryCache.clear();
|
|
99
|
+
for (const [filePath, fileInfo] of Object.entries(bundleData)) {
|
|
100
|
+
if (fileInfo.type === "binary") {
|
|
101
|
+
this.memoryCache.set(filePath, {
|
|
102
|
+
type: fileInfo.type,
|
|
103
|
+
content: Buffer.from(fileInfo.content, "base64"),
|
|
104
|
+
size: fileInfo.size
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
this.memoryCache.set(filePath, {
|
|
108
|
+
type: fileInfo.type,
|
|
109
|
+
content: fileInfo.content,
|
|
110
|
+
size: fileInfo.size
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
console.log(`📁 Loaded to memory: ${filePath} (${fileInfo.size} bytes)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.bundleLoaded = true;
|
|
117
|
+
this.bundlePath = bundlePath;
|
|
118
|
+
console.log(`✅ Bundle loaded successfully: ${this.memoryCache.size} files in memory`);
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`❌ Failed to load bundle ${bundlePath}:`, error.message);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async require(moduleName, moduleType = "auto") {
|
|
127
|
+
// 检测模块系统
|
|
128
|
+
const isESM = typeof require === "undefined";
|
|
129
|
+
|
|
130
|
+
// 如果还没有加载束,尝试加载
|
|
131
|
+
if (!this.bundleLoaded) {
|
|
132
|
+
const bundlePath = path.resolve(__dirname, "bundle.fec");
|
|
133
|
+
await this.loadBundle(bundlePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 构建文件路径
|
|
137
|
+
let targetPaths = [];
|
|
138
|
+
|
|
139
|
+
if (moduleType === "auto") {
|
|
140
|
+
// 自动检测:根据当前环境选择合适的模块
|
|
141
|
+
if (isESM) {
|
|
142
|
+
targetPaths = [
|
|
143
|
+
`esm/${moduleName}.mjs`,
|
|
144
|
+
`esm/${moduleName}.js`,
|
|
145
|
+
`cjs/${moduleName}.js`,
|
|
146
|
+
`${moduleName}.mjs`,
|
|
147
|
+
`${moduleName}.js`
|
|
148
|
+
];
|
|
149
|
+
} else {
|
|
150
|
+
targetPaths = [
|
|
151
|
+
`cjs/${moduleName}.js`,
|
|
152
|
+
`esm/${moduleName}.mjs`,
|
|
153
|
+
`${moduleName}.js`,
|
|
154
|
+
`${moduleName}.mjs`
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
} else if (moduleType === "esm") {
|
|
158
|
+
targetPaths = [`esm/${moduleName}.mjs`, `esm/${moduleName}.js`];
|
|
159
|
+
} else if (moduleType === "cjs") {
|
|
160
|
+
targetPaths = [`cjs/${moduleName}.js`];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 在内存缓存中查找文件
|
|
164
|
+
let fileInfo = null;
|
|
165
|
+
let foundPath = null;
|
|
166
|
+
|
|
167
|
+
for (const targetPath of targetPaths) {
|
|
168
|
+
if (this.memoryCache.has(targetPath)) {
|
|
169
|
+
fileInfo = this.memoryCache.get(targetPath);
|
|
170
|
+
foundPath = targetPath;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!fileInfo) {
|
|
176
|
+
throw new Error(`Module not found in bundle: ${moduleName} (tried: ${targetPaths.join(", ")})`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (fileInfo.type !== "javascript") {
|
|
180
|
+
throw new Error(`Not a JavaScript module: ${foundPath}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`🔄 Loading module from memory: ${foundPath}`);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// 执行代码
|
|
187
|
+
if (isESM) {
|
|
188
|
+
// ESM模式:使用自定义模块解析器
|
|
189
|
+
const moduleCode = fileInfo.content;
|
|
190
|
+
|
|
191
|
+
// 创建临时目录来模拟真实的文件结构
|
|
192
|
+
const tempDir = path.join(__dirname, `.temp_modules_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
|
193
|
+
try {
|
|
194
|
+
// 创建临时目录
|
|
195
|
+
if (!fs.existsSync(tempDir)) {
|
|
196
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 将所有相关模块写入临时目录
|
|
200
|
+
const moduleDir = path.dirname(foundPath);
|
|
201
|
+
for (const [filePath, fileInfo] of this.memoryCache) {
|
|
202
|
+
if (filePath.startsWith(moduleDir)) {
|
|
203
|
+
const relativePath = path.relative(moduleDir, filePath);
|
|
204
|
+
const tempFilePath = path.join(tempDir, relativePath);
|
|
205
|
+
const tempFileDir = path.dirname(tempFilePath);
|
|
206
|
+
if (!fs.existsSync(tempFileDir)) {
|
|
207
|
+
fs.mkdirSync(tempFileDir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
fs.writeFileSync(tempFilePath, fileInfo.content);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 写入当前模块
|
|
214
|
+
const tempModulePath = path.join(tempDir, path.basename(foundPath));
|
|
215
|
+
fs.writeFileSync(tempModulePath, moduleCode);
|
|
216
|
+
|
|
217
|
+
// 导入模块
|
|
218
|
+
const moduleResult = await import(`file://${tempModulePath}`);
|
|
219
|
+
return moduleResult;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.warn(`Module loading failed for ${foundPath}:`, error.message);
|
|
222
|
+
// 如果加载失败,返回一个基本的模块对象
|
|
223
|
+
return {
|
|
224
|
+
default: {},
|
|
225
|
+
__esModule: true
|
|
226
|
+
};
|
|
227
|
+
} finally {
|
|
228
|
+
// 清理临时目录
|
|
229
|
+
if (fs.existsSync(tempDir)) {
|
|
230
|
+
const cleanupDir = (dir) => {
|
|
231
|
+
const files = fs.readdirSync(dir);
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const filePath = path.join(dir, file);
|
|
234
|
+
const stat = fs.statSync(filePath);
|
|
235
|
+
if (stat.isDirectory()) {
|
|
236
|
+
cleanupDir(filePath);
|
|
237
|
+
} else {
|
|
238
|
+
fs.unlinkSync(filePath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
fs.rmdirSync(dir);
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
cleanupDir(tempDir);
|
|
245
|
+
} catch (cleanupError) {
|
|
246
|
+
// 忽略清理错误
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// CommonJS模式:使用require和vm
|
|
252
|
+
const Module = require("module");
|
|
253
|
+
const vm = require("vm");
|
|
254
|
+
const moduleCode = `(function(exports, require, module, __filename, __dirname) { ${fileInfo.content} });`;
|
|
255
|
+
const compiledWrapper = vm.runInThisContext(moduleCode, { filename: foundPath });
|
|
256
|
+
const moduleObj = { exports: {}, require: require, id: foundPath, filename: foundPath, loaded: false };
|
|
257
|
+
compiledWrapper.call(moduleObj.exports, moduleObj.exports, require, moduleObj, foundPath, path.dirname(foundPath));
|
|
258
|
+
moduleObj.loaded = true;
|
|
259
|
+
return moduleObj.exports;
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(`❌ Failed to execute module ${foundPath}:`, error.message);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 获取内存中的文件列表
|
|
268
|
+
getLoadedFiles() {
|
|
269
|
+
return Array.from(this.memoryCache.keys());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 获取内存使用统计
|
|
273
|
+
getMemoryStats() {
|
|
274
|
+
let totalSize = 0;
|
|
275
|
+
const fileTypes = new Map();
|
|
276
|
+
|
|
277
|
+
for (const [filePath, fileInfo] of this.memoryCache) {
|
|
278
|
+
totalSize += fileInfo.size;
|
|
279
|
+
const type = fileInfo.type;
|
|
280
|
+
fileTypes.set(type, (fileTypes.get(type) || 0) + 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
totalFiles: this.memoryCache.size,
|
|
285
|
+
totalSize,
|
|
286
|
+
fileTypes: Object.fromEntries(fileTypes)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 单例模式 - 确保全局只有一个decryptLoader实例
|
|
292
|
+
let _globalDecryptLoader = null;
|
|
293
|
+
|
|
294
|
+
export function createDecryptLoader(privateKeyPath) {
|
|
295
|
+
if (!_globalDecryptLoader) {
|
|
296
|
+
_globalDecryptLoader = new FuseCoreDecryptLoader(privateKeyPath);
|
|
297
|
+
}
|
|
298
|
+
return _globalDecryptLoader;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default FuseCoreDecryptLoader;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FuseCore Encrypted Entry Point
|
|
3
|
+
* 加密版本入口文件 - 解密后代理到原始API
|
|
4
|
+
*/
|
|
5
|
+
import { createDecryptLoader } from "./decrypt-loader.mjs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// 全局单例状态
|
|
15
|
+
const state = global.__FUSECORE_STATE__ || (global.__FUSECORE_STATE__ = {
|
|
16
|
+
decryptLoader: null,
|
|
17
|
+
decrypted: false,
|
|
18
|
+
originalFuseCore: null, // 解密后的原始FuseCore实例
|
|
19
|
+
eventEmitter: new EventEmitter()
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 临时文件目录
|
|
23
|
+
const TEMP_DIR = path.join(__dirname, '.temp_fusecore');
|
|
24
|
+
|
|
25
|
+
// 清理临时文件
|
|
26
|
+
function cleanupTempFiles() {
|
|
27
|
+
if (fs.existsSync(TEMP_DIR)) {
|
|
28
|
+
try {
|
|
29
|
+
const cleanupDir = (dir) => {
|
|
30
|
+
const files = fs.readdirSync(dir);
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const filePath = path.join(dir, file);
|
|
33
|
+
const stat = fs.statSync(filePath);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
cleanupDir(filePath);
|
|
36
|
+
fs.rmdirSync(filePath);
|
|
37
|
+
} else {
|
|
38
|
+
fs.unlinkSync(filePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
cleanupDir(TEMP_DIR);
|
|
43
|
+
fs.rmdirSync(TEMP_DIR);
|
|
44
|
+
console.log("🧹 Temporary files cleaned up");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn("⚠️ Failed to cleanup temporary files:", error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 重置状态函数
|
|
52
|
+
function resetState() {
|
|
53
|
+
state.decrypted = false;
|
|
54
|
+
state.originalFuseCore = null;
|
|
55
|
+
if (state.decryptLoader) {
|
|
56
|
+
state.decryptLoader = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// 核心方法
|
|
62
|
+
const initDecryption = async function(privateKeyPath) {
|
|
63
|
+
if (!privateKeyPath) {
|
|
64
|
+
throw new Error("Private key path is required for encrypted FuseCore");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!state.decryptLoader) {
|
|
68
|
+
state.decryptLoader = createDecryptLoader(privateKeyPath);
|
|
69
|
+
console.log("🔐 FuseCore decryption initialized");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 如果还没有解密,先解密所有模块
|
|
73
|
+
if (!state.decrypted) {
|
|
74
|
+
console.log("🔓 Decrypting all modules and generating temporary files...");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// 1. 加载并解密bundle
|
|
78
|
+
await state.decryptLoader.loadBundle(path.resolve(__dirname, 'bundle.fec'));
|
|
79
|
+
|
|
80
|
+
// 2. 创建临时目录
|
|
81
|
+
if (!fs.existsSync(TEMP_DIR)) {
|
|
82
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. 将所有解密后的文件写入磁盘
|
|
86
|
+
const loadedFiles = state.decryptLoader.getLoadedFiles();
|
|
87
|
+
console.log(`📁 Generating ${loadedFiles.length} temporary files...`);
|
|
88
|
+
|
|
89
|
+
for (const filePath of loadedFiles) {
|
|
90
|
+
const fileInfo = state.decryptLoader.memoryCache.get(filePath);
|
|
91
|
+
if (fileInfo && fileInfo.type === 'javascript') {
|
|
92
|
+
const tempFilePath = path.join(TEMP_DIR, filePath);
|
|
93
|
+
const tempFileDir = path.dirname(tempFilePath);
|
|
94
|
+
|
|
95
|
+
// 确保目录存在
|
|
96
|
+
if (!fs.existsSync(tempFileDir)) {
|
|
97
|
+
fs.mkdirSync(tempFileDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 写入文件
|
|
101
|
+
fs.writeFileSync(tempFilePath, fileInfo.content, 'utf8');
|
|
102
|
+
console.log(`📄 Generated: ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. 根据环境选择正确的index文件
|
|
107
|
+
let originalIndex;
|
|
108
|
+
if (typeof require === 'undefined') {
|
|
109
|
+
// ESM环境 - 加载esm/index.mjs
|
|
110
|
+
const originalIndexPath = path.join(TEMP_DIR, 'esm/index.mjs');
|
|
111
|
+
originalIndex = await import(`file://${originalIndexPath}`);
|
|
112
|
+
} else {
|
|
113
|
+
// CJS环境 - 加载cjs/index.js
|
|
114
|
+
const originalIndexPath = path.join(TEMP_DIR, 'cjs/index.js');
|
|
115
|
+
originalIndex = require(originalIndexPath);
|
|
116
|
+
}
|
|
117
|
+
state.originalFuseCore = originalIndex.default || originalIndex;
|
|
118
|
+
state.decrypted = true;
|
|
119
|
+
|
|
120
|
+
// 验证状态一致性
|
|
121
|
+
if (!state.originalFuseCore) {
|
|
122
|
+
throw new Error("Failed to load original FuseCore after decryption");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("✅ All modules decrypted and temporary files generated successfully");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("❌ Failed to decrypt modules:", error.message);
|
|
128
|
+
// 清理临时文件
|
|
129
|
+
cleanupTempFiles();
|
|
130
|
+
// 重置状态
|
|
131
|
+
resetState();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const init = async function(options = {}) {
|
|
140
|
+
if (!state.decrypted) {
|
|
141
|
+
throw new Error("FuseCore not decrypted. Call initDecryption(privateKeyPath) first.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log("🚀 Initializing FuseCore...");
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// 直接调用原始FuseCore的init方法
|
|
148
|
+
const result = await state.originalFuseCore.init(options);
|
|
149
|
+
|
|
150
|
+
console.log("✅ FuseCore initialized successfully");
|
|
151
|
+
|
|
152
|
+
// 初始化完成后清理临时文件
|
|
153
|
+
cleanupTempFiles();
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// 如果初始化失败,也要清理临时文件
|
|
158
|
+
cleanupTempFiles();
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const shutdown = async function() {
|
|
164
|
+
if (!state.decrypted) {
|
|
165
|
+
throw new Error("FuseCore not decrypted. Call initDecryption(privateKeyPath) first.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log("🔒 Shutting down FuseCore...");
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// 直接调用原始FuseCore的shutdown方法
|
|
172
|
+
await state.originalFuseCore.shutdown();
|
|
173
|
+
} finally {
|
|
174
|
+
// 确保在关闭时清理临时文件
|
|
175
|
+
cleanupTempFiles();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 创建 Proxy 对象,代理到原始 FuseCore
|
|
180
|
+
const FuseCore = new Proxy({}, {
|
|
181
|
+
get: function (target, prop) {
|
|
182
|
+
// 特殊方法直接返回
|
|
183
|
+
if (prop === "initDecryption") {
|
|
184
|
+
return initDecryption;
|
|
185
|
+
}
|
|
186
|
+
if (prop === "init") {
|
|
187
|
+
return init;
|
|
188
|
+
}
|
|
189
|
+
if (prop === "shutdown") {
|
|
190
|
+
return shutdown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 如果已经解密,代理到原始FuseCore
|
|
194
|
+
if (state.decrypted && state.originalFuseCore) {
|
|
195
|
+
return state.originalFuseCore[prop];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 状态不一致的情况处理
|
|
199
|
+
if (state.decrypted && !state.originalFuseCore) {
|
|
200
|
+
// 重置状态,强制重新解密
|
|
201
|
+
state.decrypted = false;
|
|
202
|
+
state.originalFuseCore = null;
|
|
203
|
+
throw new Error("FuseCore state corrupted. Please call initDecryption() again.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 如果还没有解密,返回错误提示
|
|
207
|
+
if (prop === "isInitialized") {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 其他属性返回错误提示
|
|
212
|
+
return function(...args) {
|
|
213
|
+
throw new Error(`FuseCore not decrypted. Call initDecryption(privateKeyPath) first.`);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export default FuseCore;
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fuse-core-express",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "FuseCore Web Component",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"postinstall": "echo \"⚠️ This is an encrypted version of FuseCore. Private key is included for MIT compliance.\""
|
|
7
|
+
},
|
|
8
|
+
"main": "index.mjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"node": {
|
|
12
|
+
"import": "./index.mjs",
|
|
13
|
+
"require": "./index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"default": "./index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"module": "./index.mjs",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ds3783/FuseCore.git"
|
|
23
|
+
},
|
|
24
|
+
"author": "Ds.3783",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ds3783/FuseCore/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/ds3783/FuseCore#readme",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"better-https-proxy-agent": "^1.0.8",
|
|
35
|
+
"cookie": "~0.7.0",
|
|
36
|
+
"dot-prop": "^9.0.0",
|
|
37
|
+
"form-data": "^4.0.0",
|
|
38
|
+
"node-cron": "^4.2.1",
|
|
39
|
+
"on-finished": "^2.3.0",
|
|
40
|
+
"redis": "^4.6.0",
|
|
41
|
+
"tracer": "^1.3.0"
|
|
42
|
+
},
|
|
43
|
+
"husky": {
|
|
44
|
+
"hooks": {
|
|
45
|
+
"pre-commit": "npm run build",
|
|
46
|
+
"pre-push": "npm test"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"type": "module",
|
|
50
|
+
"files": [
|
|
51
|
+
"index.mjs",
|
|
52
|
+
"decrypt-loader.mjs",
|
|
53
|
+
"private-1.0.2.pem",
|
|
54
|
+
"cjs/",
|
|
55
|
+
"esm/",
|
|
56
|
+
"README.md",
|
|
57
|
+
"CHANGELOG.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"keywords": [
|
|
61
|
+
"encrypted",
|
|
62
|
+
"obfuscated",
|
|
63
|
+
"protected",
|
|
64
|
+
"secure"
|
|
65
|
+
],
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=14.0.0"
|
|
68
|
+
},
|
|
69
|
+
"encrypted": {
|
|
70
|
+
"version": "1.0.2",
|
|
71
|
+
"algorithm": "AES-256-CBC + RSA-2048",
|
|
72
|
+
"obfuscated": true,
|
|
73
|
+
"integrity": "RSA-SHA256",
|
|
74
|
+
"keyRequired": "private-1.0.2.pem"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCchsSkY8vrSW1x
|
|
3
|
+
vNAuamsiSr1uvxqigGxnX/ydC3Dupl9i6FiwtyNHGE9zuQmzMOoLFcVw4XzL2EpL
|
|
4
|
+
2wjfGm3Ct4x1s7LCjbTWHjupUoTJrJI6K2WNLl4hqLQvC/Nk5RXQvJ1eVZ0RaTeb
|
|
5
|
+
a84k0QJEUYrTj1vQAk3TGFGj/k6Ch+Ia6U1zG7BlGpAbsTTDe/4ytljKplwMrPoC
|
|
6
|
+
D9DD92BIC/v6QwtyAlGXoKsiBlrVgvj4nMJii2+C/NFrcqZspoy8G7QI7bIvatwn
|
|
7
|
+
n4NNFhJefSKAfVJnxVUvQ2v25FHXCC9joSR3HGoZNIQFSx/eArCxHg4v4R1z+pyW
|
|
8
|
+
m0MLVwTjAgMBAAECggEAGrNknPRHJ1pNuJ6wVpKhtuZP/THNTdYhZ9r4YSO//n3T
|
|
9
|
+
/K/M3UxIUMrlVsTEUlRwQDbVCOX2H2HEfr8c+s3r1lDwPW3LeyOoKvRaUg55B1wQ
|
|
10
|
+
T6rydkxoE07Pec0MlnP7rjFWmO56zisIKNORhTdALz5Hs8IbM9cIxo858gMiCmL2
|
|
11
|
+
SgXoC9zjofJnztr1ZHvZ1gRTghKrXKVHOhVK8UVn1TPqVdtDAvOyIfimTuRykwMA
|
|
12
|
+
s8DrOpWu0LGUVgDXubWWSWnb0lkA81WsuHR2z0qfSkpOBtG3vpNUf5CRYofp5tZp
|
|
13
|
+
Yyab0PVoQRzVqK3Z9SY/wORs3BzQkjRRUkz8zfS9HQKBgQDPGHG/GTevkmDlhexA
|
|
14
|
+
q67112JJnP+1Y8tOYK0iO0T+XhEg3neEE1yB1NqzG4OY8XtkIAhVGoF4b4NANFD4
|
|
15
|
+
UWdbkIs4LB4LpPVYmj534wTEm8gLA9aDj3cOEOuburwnXhoSX4MYdHx/EX+vr5vU
|
|
16
|
+
FaX9+d1jafqPbxrIDEOwtAlRXwKBgQDBfUUqQ7+siRS902J1fzIukR1RiscCRYff
|
|
17
|
+
7G/KeCfdG8G7uwtXDD2ecaKvu24sAYKosZGH0kJ4ielEpoUaQeqLuXdtSFZwY2be
|
|
18
|
+
YN7bfgC2Jd6cp06Tk1IWvDgTlC9707TNpYKQJdC+1Z/EfP2hbNRwguyWYW1Pvp9h
|
|
19
|
+
WA9kM2Km/QKBgCS+hZAOfV7dbevLczuWOpjL+lhtTsF0T2I8rPth4L/xGMDoN/Rr
|
|
20
|
+
KwKuLY+R1iuQzP/a3x+acFZaEsOVhLhKWThZ0RnKWhpezllGJMdItFPeAARUTf9R
|
|
21
|
+
uSH5xpP+8dtaSu3vnGb+ZAh3plwZoGBk5urFJo65AwfoAqxz4J69ktmpAoGAeuy/
|
|
22
|
+
GE7aYx7j0oWNM+CPXQ5MdhWYwVSzMgvgKd2UVeDggAgg+DiKnTLMjKCjKcn73HiT
|
|
23
|
+
YJqwKCxVyouQXFIsVICL4x3l7Jj6LaWVcBTpFs+QUi3oudEKge8qISYv1Jd3cn8I
|
|
24
|
+
J9Qefl7xlb9i0z9059YFwLQ84kDUuN9dR+dtcXECgYAUoO4xQPtrRcWXeE0FKeBa
|
|
25
|
+
hNkJCkEF3SGIpMCFeiiOE7S0oRkPC1UteG17fKUFWMEwuyK/eKAZr4Cup7LIYzy4
|
|
26
|
+
yr2WmtgbaZO8vLaJLB4AUCsE/zU+QSQUXbsT77hcbVQxt5JJUGySYFK0UUaYw7B8
|
|
27
|
+
TjKwFwRfiCAS0Vy/yEb92Q==
|
|
28
|
+
-----END PRIVATE KEY-----
|