creative-upload-extension-updater 0.1.0

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 ADDED
@@ -0,0 +1,29 @@
1
+ # Creative Upload Extension Updater
2
+
3
+ macOS Native Messaging updater for the unpacked Creative Upload Chrome extension.
4
+
5
+ ## First installation
6
+
7
+ ```bash
8
+ npm install -g @ttam/creative-upload-extension-updater
9
+ creative-upload-extension-updater install
10
+ ```
11
+
12
+ 过渡期默认使用内网 Devbox 地址 `http://10.37.83.141:3100`,无需额外参数。其他 HTTP 地址必须显式传入 `--allow-insecure`;切换 HTTPS 后请使用 `--registry https://registry.example.com`。
13
+ The command downloads and verifies the first release, then prints the fixed extension directory. In Chrome, enable Developer mode once and choose **Load unpacked** for that directory.
14
+
15
+ The extension ID is fixed to `amfhocfbdlbooohhoinhhhgobfdmaljc`. Do not change the manifest public key after users begin using this updater.
16
+
17
+ ## Operations
18
+
19
+ ```bash
20
+ creative-upload-extension-updater diagnose
21
+ creative-upload-extension-updater update
22
+ creative-upload-extension-updater rollback
23
+ ```
24
+
25
+ `update` immediately checks the Registry and applies a newer verified release when available. On macOS it then opens a controlled extension URL so the running extension calls `chrome.runtime.reload()` automatically; pass `--no-reload` to only replace files. `rollback` restores the previous verified extension directory. Chrome must then reload the extension once.
26
+
27
+ ## Security
28
+
29
+ The updater accepts only the configured Registry URL, checks the release SHA256, verifies the detached Ed25519 signature, and verifies the manifest version and fixed Extension ID before replacing files. HTTP is a temporary internal-network transport exception; the signing private key is never stored in this package or in the Registry.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { diagnose, install, requestChromeReload, rollback, update, TRANSITION_HTTP_REGISTRY_ORIGIN } = require('../lib/updater');
5
+
6
+ const parseArguments = (values) =>
7
+ values.reduce((result, value, index) => {
8
+ if (!value.startsWith('--')) return result;
9
+ const key = value.slice(2);
10
+ const next = values[index + 1];
11
+ result[key] = next && !next.startsWith('--') ? next : true;
12
+ return result;
13
+ }, {});
14
+
15
+ const main = async () => {
16
+ const [command] = process.argv.slice(2);
17
+ const args = parseArguments(process.argv.slice(3));
18
+ if (command === 'install') {
19
+ const publicKey =
20
+ typeof args['public-key'] === 'string'
21
+ ? fs.readFileSync(path.resolve(args['public-key']), 'utf8')
22
+ : undefined;
23
+ const result = await install({
24
+ registryUrl: typeof args.registry === 'string' ? args.registry : TRANSITION_HTTP_REGISTRY_ORIGIN,
25
+ extensionId: typeof args['extension-id'] === 'string' ? args['extension-id'] : undefined,
26
+ publicKey,
27
+ allowInsecure: args['allow-insecure'] === true,
28
+ hostPath: path.resolve(__dirname, 'native-host.js'),
29
+ });
30
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
31
+ return;
32
+ }
33
+ if (command === 'diagnose') return process.stdout.write(`${JSON.stringify(diagnose(), null, 2)}\n`);
34
+ if (command === 'update') {
35
+ const result = await update();
36
+ if (result.status === 'updated' && args['no-reload'] !== true) {
37
+ const diagnostic = diagnose();
38
+ Object.assign(result, { chromeReload: requestChromeReload(diagnostic.extensionId) });
39
+ }
40
+ return process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
41
+ }
42
+ if (command === 'rollback') return process.stdout.write(`${JSON.stringify(rollback(), null, 2)}\n`);
43
+ throw new Error(
44
+ '用法:creative-upload-extension-updater install [--registry <url>] [--allow-insecure] | update [--no-reload] | diagnose | rollback。默认使用当前 Devbox 过渡地址。',
45
+ );
46
+ };
47
+
48
+ main().catch((error) => {
49
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
50
+ process.exitCode = 1;
51
+ });
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ const { checkAndUpdate } = require('../lib/updater');
3
+
4
+ let input = Buffer.alloc(0);
5
+ const respond = (value) => {
6
+ const body = Buffer.from(JSON.stringify(value));
7
+ const header = Buffer.alloc(4);
8
+ header.writeUInt32LE(body.length, 0);
9
+ process.stdout.write(Buffer.concat([header, body]));
10
+ };
11
+ process.stdin.on('data', (chunk) => {
12
+ input = Buffer.concat([input, chunk]);
13
+ if (input.length < 4) return;
14
+ const length = input.readUInt32LE(0);
15
+ if (input.length < length + 4) return;
16
+ const message = JSON.parse(input.subarray(4, length + 4).toString('utf8'));
17
+ if (message.type !== 'check_and_update')
18
+ return respond({ status: 'failed', message: '不支持的更新器请求。' });
19
+ void checkAndUpdate(message)
20
+ .then(respond)
21
+ .catch((error) =>
22
+ respond({
23
+ status: 'failed',
24
+ currentVersion: message.currentVersion,
25
+ checkedAt: Date.now(),
26
+ message: error instanceof Error ? error.message : '插件更新失败。',
27
+ }),
28
+ );
29
+ });
package/lib/updater.js ADDED
@@ -0,0 +1,171 @@
1
+ const crypto = require('node:crypto');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+
7
+ const HOST_NAME = 'com.bytedance.creative_upload_extension_updater';
8
+ const DEFAULT_EXTENSION_ID = 'amfhocfbdlbooohhoinhhhgobfdmaljc';
9
+ const TRANSITION_HTTP_REGISTRY_ORIGIN = 'http://10.37.83.141:3100';
10
+ const DEFAULT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
11
+ MCowBQYDK2VwAyEAlMYzNfVA3wLMfEniZDN3VQ+hfVXgMspwK6rM4h2P6Mk=
12
+ -----END PUBLIC KEY-----
13
+ `;
14
+ const baseDirectory = path.join(os.homedir(), 'Library', 'Application Support', 'CreativeUploadInspector');
15
+ const configPath = path.join(baseDirectory, 'updater.json');
16
+ const extensionDirectory = path.join(baseDirectory, 'extension');
17
+ const previousDirectory = path.join(baseDirectory, 'previous');
18
+ const nativeHostLauncherPath = path.join(baseDirectory, 'native-host');
19
+
20
+ const readConfig = () => {
21
+ if (!fs.existsSync(configPath)) throw new Error('更新器尚未安装。请运行 creative-upload-extension-updater install。');
22
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
23
+ };
24
+ const writeConfig = (config) => {
25
+ fs.mkdirSync(baseDirectory, { recursive: true, mode: 0o700 });
26
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
27
+ };
28
+ const assertUrl = (url, allowInsecure) => {
29
+ const parsed = new URL(url);
30
+ const transitionRegistry = parsed.origin === TRANSITION_HTTP_REGISTRY_ORIGIN;
31
+ if (parsed.protocol === 'http:' && !allowInsecure && !transitionRegistry) {
32
+ throw new Error(`仅允许 HTTPS 或当前过渡 Registry ${TRANSITION_HTTP_REGISTRY_ORIGIN}。其他 HTTP 地址需显式传入 --allow-insecure。`);
33
+ }
34
+ if (!['https:', 'http:'].includes(parsed.protocol)) {
35
+ throw new Error('更新源 URL 协议无效。');
36
+ }
37
+ return parsed;
38
+ };
39
+ const fetchJson = async (url, allowInsecure) => {
40
+ assertUrl(url, allowInsecure);
41
+ const response = await fetch(url);
42
+ if (!response.ok) throw new Error(`读取更新元数据失败(HTTP ${response.status})。`);
43
+ return response.json();
44
+ };
45
+ const extensionIdFromKey = (key) => {
46
+ const hash = crypto.createHash('sha256').update(Buffer.from(key, 'base64')).digest('hex').slice(0, 32);
47
+ return hash.replace(/[0-9a-f]/g, (digit) => 'abcdefghijklmnop'[Number.parseInt(digit, 16)]);
48
+ };
49
+ const validateRelease = (release) => {
50
+ if (!release || typeof release !== 'object') throw new Error('更新元数据格式无效。');
51
+ for (const field of ['version', 'artifactUrl', 'sha256', 'signature']) {
52
+ if (typeof release[field] !== 'string' || !release[field]) throw new Error(`更新元数据缺少 ${field}。`);
53
+ }
54
+ if (!/^[a-f0-9]{64}$/i.test(release.sha256)) throw new Error('更新元数据中的 SHA256 无效。');
55
+ };
56
+ const downloadAndVerify = async (release, config, staging) => {
57
+ assertUrl(release.artifactUrl, config.allowInsecure);
58
+ const response = await fetch(release.artifactUrl);
59
+ if (!response.ok) throw new Error(`下载插件制品失败(HTTP ${response.status})。`);
60
+ const content = Buffer.from(await response.arrayBuffer());
61
+ const checksum = crypto.createHash('sha256').update(content).digest('hex');
62
+ if (!crypto.timingSafeEqual(Buffer.from(checksum), Buffer.from(release.sha256))) throw new Error('插件制品 SHA256 校验失败。');
63
+ if (!crypto.verify(null, content, config.signingPublicKey, Buffer.from(release.signature, 'base64'))) throw new Error('插件制品签名校验失败。');
64
+ const archive = path.join(staging, 'extension.zip');
65
+ fs.writeFileSync(archive, content, { mode: 0o600 });
66
+ return archive;
67
+ };
68
+ const unpackAndValidate = (archive, staging, release, config) => {
69
+ const unpacked = path.join(staging, 'unpacked');
70
+ fs.mkdirSync(unpacked, { recursive: true, mode: 0o700 });
71
+ execFileSync('/usr/bin/ditto', ['-x', '-k', archive, unpacked], { stdio: 'pipe' });
72
+ const manifestPath = path.join(unpacked, 'manifest.json');
73
+ if (!fs.existsSync(manifestPath)) throw new Error('插件制品缺少 manifest.json。');
74
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
75
+ if (manifest.version !== release.version) throw new Error('插件制品版本与发布元数据不一致。');
76
+ if (typeof manifest.key !== 'string' || extensionIdFromKey(manifest.key) !== config.extensionId) throw new Error('插件制品的 Extension ID 不匹配。');
77
+ return unpacked;
78
+ };
79
+ const replaceExtension = (unpacked) => {
80
+ const failedDirectory = `${extensionDirectory}.failed-${Date.now()}`;
81
+ fs.rmSync(failedDirectory, { recursive: true, force: true });
82
+ try {
83
+ if (fs.existsSync(previousDirectory)) fs.rmSync(previousDirectory, { recursive: true, force: true });
84
+ if (fs.existsSync(extensionDirectory)) fs.renameSync(extensionDirectory, previousDirectory);
85
+ fs.renameSync(unpacked, extensionDirectory);
86
+ } catch (error) {
87
+ if (!fs.existsSync(extensionDirectory) && fs.existsSync(previousDirectory)) fs.renameSync(previousDirectory, extensionDirectory);
88
+ throw error;
89
+ }
90
+ };
91
+ const checkAndUpdate = async (request) => {
92
+ const config = readConfig();
93
+ if (request.extensionId !== config.extensionId) throw new Error('未授权的扩展尝试调用更新器。');
94
+ const response = await fetchJson(`${config.registryUrl}/v1/extension-releases/latest`, config.allowInsecure);
95
+ const checkedAt = Date.now();
96
+ if (!response.release) return { status: 'up_to_date', currentVersion: request.currentVersion, checkedAt, message: 'Registry 尚未发布插件版本。' };
97
+ const release = response.release;
98
+ validateRelease(release);
99
+ if (release.version === request.currentVersion) return { status: 'up_to_date', currentVersion: request.currentVersion, targetVersion: release.version, checkedAt };
100
+ const staging = fs.mkdtempSync(path.join(baseDirectory, 'staging-'));
101
+ try {
102
+ const archive = await downloadAndVerify(release, config, staging);
103
+ const unpacked = unpackAndValidate(archive, staging, release, config);
104
+ replaceExtension(unpacked);
105
+ return { status: 'updated', currentVersion: request.currentVersion, targetVersion: release.version, checkedAt };
106
+ } finally {
107
+ fs.rmSync(staging, { recursive: true, force: true });
108
+ }
109
+ };
110
+ const update = async () => {
111
+ const config = readConfig();
112
+ const manifestPath = path.join(extensionDirectory, 'manifest.json');
113
+ if (!fs.existsSync(manifestPath)) throw new Error('未找到已安装的插件目录。请先运行 creative-upload-extension-updater install。');
114
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
115
+ if (typeof manifest.version !== 'string') throw new Error('已安装插件的 manifest 版本无效。');
116
+ return checkAndUpdate({ extensionId: config.extensionId, currentVersion: manifest.version });
117
+ };
118
+ const requestChromeReload = (extensionId) => {
119
+ if (process.platform !== 'darwin') return { requested: false, message: '当前平台不支持命令行自动重载。' };
120
+ try {
121
+ execFileSync('/usr/bin/open', ['-g', '-a', 'Google Chrome', `chrome-extension://${extensionId}/options.html?nativeReload=1`], { stdio: 'ignore' });
122
+ return { requested: true };
123
+ } catch {
124
+ return { requested: false, message: '未能唤起 Google Chrome;请在 chrome://extensions 手动重新加载。' };
125
+ }
126
+ };
127
+ const installNativeHostLauncher = (hostPath) => {
128
+ const nodePath = process.execPath;
129
+ if (!path.isAbsolute(nodePath)) throw new Error('无法确定 Native Host 的 Node 运行时路径。');
130
+ if (!path.isAbsolute(hostPath)) throw new Error('Native Host 脚本路径必须为绝对路径。');
131
+
132
+ // Chrome does not inherit an interactive shell's PATH, so /usr/bin/env node
133
+ // cannot resolve an nvm-managed Node binary. Keep a stable launcher with the
134
+ // exact Node executable used during installation.
135
+ fs.mkdirSync(baseDirectory, { recursive: true, mode: 0o700 });
136
+ fs.writeFileSync(
137
+ nativeHostLauncherPath,
138
+ `#!/bin/sh\nexec ${JSON.stringify(nodePath)} ${JSON.stringify(hostPath)}\n`,
139
+ { mode: 0o700 },
140
+ );
141
+ fs.chmodSync(nativeHostLauncherPath, 0o700);
142
+ return nativeHostLauncherPath;
143
+ };
144
+ const install = async ({ registryUrl = TRANSITION_HTTP_REGISTRY_ORIGIN, extensionId = DEFAULT_EXTENSION_ID, publicKey = DEFAULT_PUBLIC_KEY, allowInsecure = false, hostPath }) => {
145
+ assertUrl(registryUrl, allowInsecure);
146
+ if (!/^[a-p]{32}$/.test(extensionId)) throw new Error('Extension ID 无效。');
147
+ crypto.createPublicKey(publicKey);
148
+ const config = { registryUrl: registryUrl.replace(/\/$/, ''), extensionId, signingPublicKey: publicKey, allowInsecure };
149
+ writeConfig(config);
150
+ const update = await checkAndUpdate({ extensionId, currentVersion: '0.0.0' });
151
+ const manifestDirectory = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts');
152
+ fs.mkdirSync(manifestDirectory, { recursive: true, mode: 0o700 });
153
+ const launcherPath = installNativeHostLauncher(hostPath);
154
+ fs.writeFileSync(path.join(manifestDirectory, `${HOST_NAME}.json`), `${JSON.stringify({ name: HOST_NAME, description: 'Creative Upload Extension Updater', path: launcherPath, type: 'stdio', allowed_origins: [`chrome-extension://${extensionId}/`] }, null, 2)}\n`, { mode: 0o600 });
155
+ return { ...update, extensionDirectory, extensionId };
156
+ };
157
+ const rollback = () => {
158
+ if (!fs.existsSync(previousDirectory)) throw new Error('没有可回滚的上一版本。');
159
+ const currentDirectory = `${extensionDirectory}.rollback-${Date.now()}`;
160
+ if (fs.existsSync(extensionDirectory)) fs.renameSync(extensionDirectory, currentDirectory);
161
+ fs.renameSync(previousDirectory, extensionDirectory);
162
+ fs.rmSync(currentDirectory, { recursive: true, force: true });
163
+ return { extensionDirectory };
164
+ };
165
+ const diagnose = () => {
166
+ const config = readConfig();
167
+ const manifestPath = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
168
+ return { extensionDirectory, extensionLoaded: fs.existsSync(path.join(extensionDirectory, 'manifest.json')), nativeHostRegistered: fs.existsSync(manifestPath), registryUrl: config.registryUrl, extensionId: config.extensionId };
169
+ };
170
+
171
+ module.exports = { DEFAULT_EXTENSION_ID, HOST_NAME, TRANSITION_HTTP_REGISTRY_ORIGIN, checkAndUpdate, diagnose, install, requestChromeReload, rollback, update };
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "creative-upload-extension-updater",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Native Messaging updater for the Creative Upload unpacked Chrome extension.",
6
+ "bin": {
7
+ "creative-upload-updater": "bin/creative-upload-extension-updater.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ }
12
+ }