domains007 1.0.1
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/README.md +160 -0
- package/domains.json +5 -0
- package/index.d.ts +39 -0
- package/index.js +76 -0
- package/index.mjs +47 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# keep-alive-domains
|
|
2
|
+
|
|
3
|
+
Base64 编码的 API 域名配置包,用于 Android App 域名容灾。
|
|
4
|
+
|
|
5
|
+
## 核心思路
|
|
6
|
+
|
|
7
|
+
> **用 npm + CDN 作为免费、高可用的域名配置分发通道,让 Android App 在域名挂了的时候能自我修复。**
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
你的操作(域名挂了时):
|
|
11
|
+
node encode.js → 改 domains.json → npm publish
|
|
12
|
+
│
|
|
13
|
+
▼
|
|
14
|
+
jsdelivr / unpkg CDN 自动更新(1-5分钟)
|
|
15
|
+
│
|
|
16
|
+
▼
|
|
17
|
+
Android App:
|
|
18
|
+
KeepAliveManager.request { domain -> ... }
|
|
19
|
+
│
|
|
20
|
+
├─ 1. 用域名发起 okhttp 请求
|
|
21
|
+
├─ 2. 失败 → CDN 拉取 domains.json
|
|
22
|
+
├─ 3. base64 解码 payload → 获得新域名
|
|
23
|
+
├─ 4. 新域名重试 → 成功
|
|
24
|
+
└─ 5. SharedPreferences 缓存
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 为什么这个方案成立
|
|
28
|
+
|
|
29
|
+
npm + CDN(unpkg / jsdelivr)的基础设施可用性远超你自己某个 API 域名:
|
|
30
|
+
|
|
31
|
+
| 渠道 | 被墙/挂掉概率 | 速度 | 成本 |
|
|
32
|
+
| ------ | :---------: | ------ | ------ |
|
|
33
|
+
| 你自己的 API 域名 | 高(这就是问题) | — | 服务器成本 |
|
|
34
|
+
| `npmjs.com` | 极低 | 全球 CDN | **免费** |
|
|
35
|
+
| `unpkg.com` | 极低 | Cloudflare | **免费** |
|
|
36
|
+
| `jsdelivr.net` | 极低(国内可用) | 全球 + 国内节点 | **免费** |
|
|
37
|
+
|
|
38
|
+
App 同时配 3 个 CDN 地址作为 fallback,几乎不可能全挂。
|
|
39
|
+
|
|
40
|
+
## 项目结构
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
keep-alive-domains/
|
|
44
|
+
├── package.json # name, version, 无依赖
|
|
45
|
+
├── domains.json # ★ 核心:base64 编码的域名配置
|
|
46
|
+
├── index.js # CommonJS 入口,自动解码
|
|
47
|
+
├── index.mjs # ESM 入口
|
|
48
|
+
├── index.d.ts # TypeScript 类型定义
|
|
49
|
+
├── encode.js # 工具脚本:编码新域名
|
|
50
|
+
├── .npmignore
|
|
51
|
+
└── README.md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 关键设计
|
|
55
|
+
|
|
56
|
+
| 设计点 | 实现 |
|
|
57
|
+
| ------ | ------ |
|
|
58
|
+
| **域名不暴露** | `domains.json` 的 `payload` 是 base64 编码的 JSON,CDN 上浏览看不到明文 |
|
|
59
|
+
| **多 CDN 容灾** | jsdelivr + jsdelivr@latest + unpkg,三个地址依次尝试 |
|
|
60
|
+
| **三级降级** | CDN → 本地 SharedPreferences 缓存 → 编译期内置兜底 |
|
|
61
|
+
| **版本比较** | `domains.json` 的 `version` 递增,App 比较后才更新本地缓存 |
|
|
62
|
+
| **Android 单例** | `KeepAliveManager` 是 Kotlin `object`,启动时 init 一次,全局可用 |
|
|
63
|
+
|
|
64
|
+
## domains.json 设计
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"version": 1,
|
|
69
|
+
"updatedAt": "2026-07-25T00:00:00Z",
|
|
70
|
+
"payload": "eyJwcmltYXJ5Ijoi..."
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`payload` 是 base64 编码的 JSON,解码后:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"primary": "https://api.example.com",
|
|
79
|
+
"fallbacks": [
|
|
80
|
+
"https://api2.example.com",
|
|
81
|
+
"https://api3.example.com"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage (Node.js)
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const domains = require('keep-alive-domains');
|
|
90
|
+
console.log(domains.getPrimary()); // "https://api.example.com"
|
|
91
|
+
console.log(domains.getAll()); // ["https://api.example.com", ...]
|
|
92
|
+
console.log(domains.getFallbacks()); // ["https://api2.example.com", ...]
|
|
93
|
+
console.log(domains.getVersion()); // 1
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 发布步骤
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 1. 修改域名
|
|
100
|
+
node encode.js # 先编辑 encode.js 里的 config 对象,再运行,复制输出
|
|
101
|
+
|
|
102
|
+
# 2. 更新 domains.json
|
|
103
|
+
# 粘贴上面输出的内容,递增 version
|
|
104
|
+
|
|
105
|
+
# 3. 发版
|
|
106
|
+
npm version patch # 1.0.0 → 1.0.1
|
|
107
|
+
npm publish
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Android: Fetch from CDN
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
https://cdn.jsdelivr.net/npm/keep-alive-domains/domains.json
|
|
114
|
+
https://cdn.jsdelivr.net/npm/keep-alive-domains@latest/domains.json
|
|
115
|
+
https://unpkg.com/keep-alive-domains@latest/domains.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 进阶方向
|
|
119
|
+
|
|
120
|
+
- **签名校验**:对 payload 加 HMAC 签名,App 内置公钥验签,防止 CDN 被劫持篡改
|
|
121
|
+
- **多渠道**:按 `production` / `staging` 区分不同域名
|
|
122
|
+
- **灰度发布**:利用 npm dist-tag(`npm publish --tag beta`)实现按 tag 拉不同版本
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
┌─────────────────────────────────────────────────────────┐
|
|
130
|
+
│ 你的本地(仅你可见) │
|
|
131
|
+
│ │
|
|
132
|
+
│ 1. 编辑 encode.js │
|
|
133
|
+
│ const config = { │
|
|
134
|
+
│ primary: 'https://你的新域名.com', │
|
|
135
|
+
│ fallbacks: [...] │
|
|
136
|
+
│ } │
|
|
137
|
+
│ │
|
|
138
|
+
│ 2. node encode.js │
|
|
139
|
+
│ → 输出新的 base64 payload │
|
|
140
|
+
│ │
|
|
141
|
+
│ 3. 粘贴到 domains.json,version +1 │
|
|
142
|
+
│ │
|
|
143
|
+
│ 4. npm version patch && npm publish │
|
|
144
|
+
│ → 发布到 npmjs.com(domains.json 被打包进去) │
|
|
145
|
+
│ │
|
|
146
|
+
│ 5. git add . && git commit && git push │
|
|
147
|
+
│ → 推送到 Gitee(不含 encode.js / domains.json / README) │
|
|
148
|
+
└─────────────────────────────────────────────────────────┘
|
|
149
|
+
│
|
|
150
|
+
▼
|
|
151
|
+
npmjs.com ──▶ CDN 自动同步 ──▶ Android App 拉取新域名
|
|
152
|
+
|
|
153
|
+
步骤 命令 说明
|
|
154
|
+
① 编辑 encode.js 改 config 里的域名
|
|
155
|
+
② node encode.js 生成 base64 payload
|
|
156
|
+
③ 粘贴到 domains.json 替换 payload,version 递增
|
|
157
|
+
④ npm version patch 包版本号自动 +1(如 1.0.1 → 1.0.2)
|
|
158
|
+
⑤ npm publish 发布,domains.json 随包上传
|
|
159
|
+
⑥ git add . && git commit && git push Gitee 只接收骨架代码变化
|
|
160
|
+
关键点:npm publish 看的是本地文件系统,不是 git。所以 domains.json 即使被 .gitignore 排除,它仍然会被 package.json 的 "files": [...] 打包发布到 npm。Gitee 干净,npm 正常用。
|
package/domains.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"updatedAt": "2026-08-14T11:34:38.550Z",
|
|
4
|
+
"payload": "eyJjaGFubmVscyI6eyJid2NqIjp7InByaW1hcnkiOiJodHRwczovL3dzLTA4MDgtbG9uZ3RpbWUtem4tYndjai15bWFwaS5rbHF0dnhkLmNvbSIsImZhbGxiYWNrcyI6WyJodHRwczovL3dzLTA4MDgtbG9uZ3RpbWUtem4tYndjai15bWFwaS5rbHF0dnhkLmNvbSJdfSwiZHlwaiI6eyJwcmltYXJ5IjoiaHR0cHM6Ly9hcGktb3RoZXIuZXhhbXBsZS5jb20iLCJmYWxsYmFja3MiOlsiaHR0cHM6Ly9hcGktb3RoZXIyLmV4YW1wbGUuY29tIl19LCJwZGQiOnsicHJpbWFyeSI6Imh0dHBzOi8vYXBpLW90aGVyLmV4YW1wbGUuY29tIiwiZmFsbGJhY2tzIjpbImh0dHBzOi8vYXBpLW90aGVyMi5leGFtcGxlLmNvbSJdfSwieGNoYXQiOnsicHJpbWFyeSI6Imh0dHBzOi8vd3MtMDgwOC1sb25ndGltZS16bi14Y2hhdC15bWFwaS5rbHF0dnhkLmNvbSIsImZhbGxiYWNrcyI6WyJodHRwczovL3dzLTA4MDgtbG9uZ3RpbWUtem4teGNoYXQteW1hcGkua2xxdHZ4ZC5jb20iXX0sInhzbSI6eyJwcmltYXJ5IjoiaHR0cHM6Ly9hcGktb3RoZXIuZXhhbXBsZS5jb20iLCJmYWxsYmFja3MiOlsiaHR0cHM6Ly9hcGktb3RoZXIyLmV4YW1wbGUuY29tIl19fX0="
|
|
5
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
declare module 'keep-alive-domains' {
|
|
2
|
+
/** Single channel domain config */
|
|
3
|
+
interface ChannelConfig {
|
|
4
|
+
primary: string;
|
|
5
|
+
fallbacks: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get config for a specific channel.
|
|
10
|
+
* @returns null if channel not found
|
|
11
|
+
*/
|
|
12
|
+
export function getChannel(name: string): ChannelConfig | null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get all channel names.
|
|
16
|
+
*/
|
|
17
|
+
export function getChannelNames(): string[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the config version number for cache comparison.
|
|
21
|
+
*/
|
|
22
|
+
export function getVersion(): number;
|
|
23
|
+
|
|
24
|
+
/** Config version */
|
|
25
|
+
export const version: number;
|
|
26
|
+
|
|
27
|
+
/** Last update timestamp (ISO 8601) */
|
|
28
|
+
export const updatedAt: string;
|
|
29
|
+
|
|
30
|
+
/** Raw channels map */
|
|
31
|
+
export const channels: Record<string, ChannelConfig>;
|
|
32
|
+
|
|
33
|
+
const _default: {
|
|
34
|
+
version: number;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
channels: Record<string, ChannelConfig>;
|
|
37
|
+
};
|
|
38
|
+
export default _default;
|
|
39
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keep-alive-domains
|
|
3
|
+
*
|
|
4
|
+
* Decodes base64-encoded channel-based domain configuration from domains.json.
|
|
5
|
+
* Usage:
|
|
6
|
+
* const domains = require('keep-alive-domains');
|
|
7
|
+
* const ch = domains.getChannel('channel1');
|
|
8
|
+
* console.log(ch.primary); // "https://api.example.com"
|
|
9
|
+
* console.log(ch.fallbacks); // ["https://api2.example.com", ...]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const raw = require('./domains.json');
|
|
13
|
+
|
|
14
|
+
function decodeBase64(str) {
|
|
15
|
+
if (typeof Buffer !== 'undefined') {
|
|
16
|
+
return Buffer.from(str, 'base64').toString('utf-8');
|
|
17
|
+
}
|
|
18
|
+
if (typeof atob !== 'undefined') {
|
|
19
|
+
return atob(str);
|
|
20
|
+
}
|
|
21
|
+
throw new Error('No base64 decoder available');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseConfig(rawConfig) {
|
|
25
|
+
if (!rawConfig || !rawConfig.payload) {
|
|
26
|
+
throw new Error('Invalid domains.json: missing payload');
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const decoded = decodeBase64(rawConfig.payload);
|
|
30
|
+
const config = JSON.parse(decoded);
|
|
31
|
+
return {
|
|
32
|
+
version: rawConfig.version,
|
|
33
|
+
updatedAt: rawConfig.updatedAt,
|
|
34
|
+
...config,
|
|
35
|
+
};
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error('Failed to decode domain config: ' + e.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = parseConfig(raw);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get config for a specific channel.
|
|
45
|
+
* @param {string} name - channel name, e.g. "channel1"
|
|
46
|
+
* @returns {{ primary: string, fallbacks: string[] } | null}
|
|
47
|
+
*/
|
|
48
|
+
function getChannel(name) {
|
|
49
|
+
if (!config.channels || !config.channels[name]) return null;
|
|
50
|
+
return config.channels[name];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all channel names.
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function getChannelNames() {
|
|
58
|
+
return Object.keys(config.channels || {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the config version number (for cache comparison).
|
|
63
|
+
*/
|
|
64
|
+
function getVersion() {
|
|
65
|
+
return config.version;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
getChannel,
|
|
70
|
+
getChannelNames,
|
|
71
|
+
getVersion,
|
|
72
|
+
version: config.version,
|
|
73
|
+
updatedAt: config.updatedAt,
|
|
74
|
+
// Raw access if needed
|
|
75
|
+
channels: config.channels || {},
|
|
76
|
+
};
|
package/index.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keep-alive-domains — ESM entry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const raw = JSON.parse(readFileSync(join(__dirname, 'domains.json'), 'utf-8'));
|
|
11
|
+
|
|
12
|
+
function decodeBase64(str) {
|
|
13
|
+
return Buffer.from(str, 'base64').toString('utf-8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseConfig(rawConfig) {
|
|
17
|
+
if (!rawConfig || !rawConfig.payload) {
|
|
18
|
+
throw new Error('Invalid domains.json: missing payload');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const decoded = decodeBase64(rawConfig.payload);
|
|
22
|
+
const config = JSON.parse(decoded);
|
|
23
|
+
return { version: rawConfig.version, updatedAt: rawConfig.updatedAt, ...config };
|
|
24
|
+
} catch (e) {
|
|
25
|
+
throw new Error('Failed to decode domain config: ' + e.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const config = parseConfig(raw);
|
|
30
|
+
|
|
31
|
+
export function getChannel(name) {
|
|
32
|
+
if (!config.channels || !config.channels[name]) return null;
|
|
33
|
+
return config.channels[name];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getChannelNames() {
|
|
37
|
+
return Object.keys(config.channels || {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getVersion() {
|
|
41
|
+
return config.version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const version = config.version;
|
|
45
|
+
export const updatedAt = config.updatedAt;
|
|
46
|
+
export const channels = config.channels || {};
|
|
47
|
+
export default config;
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "domains007",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "API domain configuration with base64 encoding for Android app failover",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"index.d.ts",
|
|
12
|
+
"domains.json"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://gitee.com/ggkk23/mydomains.git"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"keep-alive",
|
|
23
|
+
"domain",
|
|
24
|
+
"failover",
|
|
25
|
+
"api",
|
|
26
|
+
"android"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|