@dreamor/atlas-cli 0.7.21 → 0.7.23

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 便携登录态 bundle(对齐 dws portableAuthBundleManifest)。
3
+ *
4
+ * bundle 结构(gzip + JSON 明文,复用 cookies 已明文的事实):
5
+ *
6
+ * atlas-auth.tar.gz(实际是 gzip(JSON) 单文件,非真 tar — 简化实现)
7
+ * └── { manifest: {...}, cookies: [...] }
8
+ *
9
+ * base64 模式 = base64(gzip(JSON)),方便通过 env / 管道传输。
10
+ *
11
+ * 安全:--refresh-only 默认开启,剥离短命 token(access_token* / refresh_token*),
12
+ * 只保留 SSO_REFRESH_TOKEN + buc_* + 身份相关 cookies。导出文件权限 0600
13
+ * (secureWriteFile)。
14
+ *
15
+ * 零运行时依赖:仅用 node:zlib + secure-fs。
16
+ */
17
+ import { gzip, gunzip } from 'zlib';
18
+ import { promisify } from 'util';
19
+ import { readFile, mkdir } from 'fs/promises';
20
+ import { dirname, resolve } from 'path';
21
+ import { existsSync } from 'fs';
22
+ import { secureWriteFile } from './secure-fs.js';
23
+ import { ConfigError } from './errors.js';
24
+ import { platform } from 'os';
25
+ const gzipAsync = promisify(gzip);
26
+ const gunzipAsync = promisify(gunzip);
27
+ export const BUNDLE_SCHEMA = 'atlas.auth.bundle/v1';
28
+ export const BUNDLE_VERSION = 1;
29
+ /** 短命 token cookie 名集合(--refresh-only 时剥离) */
30
+ const EPHEMERAL_TOKEN_NAMES = new Set([
31
+ 'access_token',
32
+ 'access_token.sig',
33
+ 'refresh_token',
34
+ 'refresh_token.sig',
35
+ ]);
36
+ /** 构造 bundle(纯函数) */
37
+ export function buildBundle(cookies, opts) {
38
+ const filtered = opts.refreshOnly === false
39
+ ? cookies
40
+ : cookies.filter((c) => !EPHEMERAL_TOKEN_NAMES.has(c.name));
41
+ return {
42
+ manifest: {
43
+ schema: BUNDLE_SCHEMA,
44
+ version: BUNDLE_VERSION,
45
+ os: opts.os ?? platform(),
46
+ exportedAt: opts.exportedAt,
47
+ refreshOnly: opts.refreshOnly,
48
+ },
49
+ cookies: filtered,
50
+ };
51
+ }
52
+ /** 序列化为 gzip + base64 字符串(适合 --base64 stdout) */
53
+ export async function serializeBundleBase64(bundle) {
54
+ const json = JSON.stringify(bundle);
55
+ const zipped = await gzipAsync(Buffer.from(json, 'utf-8'));
56
+ return zipped.toString('base64');
57
+ }
58
+ /** 序列化为 gzip Buffer(适合写文件) */
59
+ export async function serializeBundleBuffer(bundle) {
60
+ const json = JSON.stringify(bundle);
61
+ return gzipAsync(Buffer.from(json, 'utf-8'));
62
+ }
63
+ /** 校验 manifest schema/version,失败抛 ConfigError */
64
+ function assertManifest(m) {
65
+ if (!m || typeof m !== 'object') {
66
+ throw new ConfigError('无效 bundle:manifest 缺失');
67
+ }
68
+ const o = m;
69
+ if (o.schema !== BUNDLE_SCHEMA) {
70
+ throw new ConfigError(`无效 bundle:schema 不匹配(期望 ${BUNDLE_SCHEMA},实际 ${String(o.schema)})`);
71
+ }
72
+ if (typeof o.version !== 'number' || o.version > BUNDLE_VERSION) {
73
+ throw new ConfigError(`无效 bundle:版本不支持(当前 ${BUNDLE_VERSION},bundle ${String(o.version)})`);
74
+ }
75
+ if (!Array.isArray(o.cookies) === false && !o.cookies === undefined) {
76
+ // bundles with cookies at top handled below; manifest-only ok
77
+ }
78
+ }
79
+ /** 解析 base64 字符串 → AuthBundle */
80
+ export async function parseBundleBase64(b64) {
81
+ const buf = Buffer.from(b64.trim(), 'base64');
82
+ let json;
83
+ try {
84
+ json = (await gunzipAsync(buf)).toString('utf-8');
85
+ }
86
+ catch {
87
+ // 兼容未压缩的纯 base64 JSON(少数手动构造场景)
88
+ try {
89
+ json = buf.toString('utf-8');
90
+ }
91
+ catch {
92
+ throw new ConfigError('无效 bundle:base64 解码失败');
93
+ }
94
+ }
95
+ return parseBundleJson(json);
96
+ }
97
+ /** 解析文件(gzip 二进制 或 base64 文本)→ AuthBundle */
98
+ export async function parseBundleFile(path) {
99
+ if (!existsSync(path)) {
100
+ throw new ConfigError(`导入文件不存在:${path}`);
101
+ }
102
+ const raw = await readFile(path);
103
+ // 二进制 gzip 文件(前两字节 0x1f 0x8b)
104
+ if (raw.length >= 2 && raw[0] === 0x1f && raw[1] === 0x8b) {
105
+ const json = (await gunzipAsync(raw)).toString('utf-8');
106
+ return parseBundleJson(json);
107
+ }
108
+ // 否则按 base64 文本处理
109
+ return parseBundleBase64(raw.toString('utf-8'));
110
+ }
111
+ function parseBundleJson(json) {
112
+ let obj;
113
+ try {
114
+ obj = JSON.parse(json);
115
+ }
116
+ catch {
117
+ throw new ConfigError('无效 bundle:JSON 解析失败');
118
+ }
119
+ if (!obj || typeof obj !== 'object') {
120
+ throw new ConfigError('无效 bundle:根节点非对象');
121
+ }
122
+ const o = obj;
123
+ assertManifest(o.manifest);
124
+ if (!Array.isArray(o.cookies)) {
125
+ throw new ConfigError('无效 bundle:cookies 字段非数组');
126
+ }
127
+ const bundle = {
128
+ manifest: o.manifest,
129
+ cookies: o.cookies,
130
+ };
131
+ const currentOs = platform();
132
+ return {
133
+ bundle,
134
+ warning: {
135
+ osMismatch: bundle.manifest.os !== currentOs,
136
+ refreshOnly: bundle.manifest.refreshOnly,
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * 写 bundle 到文件路径(secureWriteFile 0600)。
142
+ *
143
+ * 与 baseline/actual export 的 --out 白名单(resolveSecureExportPath)不同:
144
+ * auth export 输出的是便携凭证 bundle,用户主动指定任意路径(如 /tmp 方便传输
145
+ * 到沙盒)是其核心用例,因此放宽为绝对/相对路径均可,但仍以 0600 写入。
146
+ */
147
+ export async function writeBundleToFile(bundle, outPath, base64) {
148
+ const resolved = resolve(process.cwd(), outPath);
149
+ const content = base64 ? await serializeBundleBase64(bundle) : await serializeBundleBuffer(bundle);
150
+ await mkdir(dirname(resolved), { recursive: true });
151
+ await secureWriteFile(resolved, content);
152
+ return resolved;
153
+ }
@@ -1 +1 @@
1
- export const ATLAS_VERSION = '0.7.21';
1
+ export const ATLAS_VERSION = '0.7.23';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamor/atlas-cli",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
4
4
  "description": "Atlas CLI - 斑马云图人力基线管理工具",
5
5
  "type": "module",
6
6
  "bin": {