py-auth-client 0.1.3 → 0.1.5

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 CHANGED
@@ -1,14 +1,14 @@
1
- # py-auth-client
1
+ # py-auth-client(TypeScript / Node.js)
2
2
 
3
- TypeScript/Node.js client for **py-auth**. Compatible with the Python and Go clients in this repository.
3
+ 与仓库内 PythonGo 客户端协议一致。安装与最小示例见下;**完整文档(配置、缓存、存储、`device_info`)见上一级 [client/README.md](../README.md#typescript)**(与 [STORAGE.md](../STORAGE.md) 对照阅读)。
4
4
 
5
- ## Install
5
+ ## 安装
6
6
 
7
7
  ```bash
8
8
  npm i py-auth-client
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## 最小示例
12
12
 
13
13
  ```ts
14
14
  import { AuthClient, AuthorizationError } from "py-auth-client";
@@ -16,13 +16,11 @@ import { AuthClient, AuthorizationError } from "py-auth-client";
16
16
  const client = new AuthClient({
17
17
  serverUrl: "http://localhost:8000",
18
18
  softwareName: "我的软件",
19
- clientSecret: process.env.CLIENT_SECRET!,
20
- // debug: true,
19
+ clientSecret: process.env.CLIENT_SECRET ?? "",
21
20
  });
22
21
 
23
22
  try {
24
23
  await client.requireAuthorization();
25
- console.log("authorized");
26
24
  } catch (e) {
27
25
  if (e instanceof AuthorizationError) {
28
26
  console.error(e.message);
@@ -31,17 +29,3 @@ try {
31
29
  throw e;
32
30
  }
33
31
  ```
34
-
35
- ## API
36
-
37
- - `new AuthClient(config)`
38
- - `client.checkAuthorization()`
39
- - `client.requireAuthorization()`
40
- - `client.getAuthorizationInfo()`
41
- - `client.clearCache()`
42
-
43
- ## Notes
44
-
45
- - Requires `clientSecret` or environment variable `CLIENT_SECRET`.
46
- - Uses Fernet-compatible encryption derived from `CLIENT_SECRET`.
47
- - Uses an obfuscated local cache file with a 7-day validity window.
package/dist/cache.d.ts CHANGED
@@ -1,24 +1,30 @@
1
- import type { CacheRecord } from "./types";
1
+ import type { CacheRecord, DeviceInfo } from "./types";
2
2
  export declare class AuthCache {
3
3
  readonly cacheValiditySeconds: number;
4
4
  readonly checkIntervalSeconds: number;
5
5
  readonly cacheFile: string;
6
- private readonly encryptKey;
7
6
  private readonly deviceId;
7
+ private readonly serverUrl;
8
8
  private readonly softwareName;
9
- private readonly cacheValidityDays;
9
+ private readonly cacheDir;
10
10
  constructor(args: {
11
- cacheDir?: string;
11
+ storageRoot: string;
12
12
  deviceId: string;
13
13
  serverUrl: string;
14
14
  softwareName: string;
15
15
  cacheValidityDays: number;
16
16
  checkIntervalDays: number;
17
17
  });
18
+ private readRow;
19
+ private cacheRecordFromRow;
20
+ snapshotForAuthorizationCheck(): {
21
+ cacheData: CacheRecord | null;
22
+ storedHeartbeatTimes: number;
23
+ };
24
+ isCacheTTLValid(cache: CacheRecord | null): boolean;
25
+ needsCheck(): boolean;
18
26
  getCache(): CacheRecord | null;
19
- saveCache(authorized: boolean, message: string): boolean;
20
- isCacheValid(): boolean;
27
+ loadDeviceInfoSnapshot(): DeviceInfo | null;
28
+ saveCache(authorized: boolean, _message: string, nextHeartbeat?: number, deviceInfoSnapshot?: DeviceInfo): boolean;
21
29
  clearCache(): boolean;
22
- private obfuscate;
23
- private deobfuscate;
24
30
  }
package/dist/cache.js CHANGED
@@ -4,201 +4,179 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AuthCache = void 0;
7
- const node_os_1 = __importDefault(require("node:os"));
8
- const node_path_1 = __importDefault(require("node:path"));
9
7
  const node_fs_1 = __importDefault(require("node:fs"));
10
- const node_crypto_1 = __importDefault(require("node:crypto"));
11
- const node_zlib_1 = __importDefault(require("node:zlib"));
12
- const node_child_process_1 = require("node:child_process");
8
+ const stateBundle_1 = require("./stateBundle");
13
9
  class AuthCache {
14
10
  cacheValiditySeconds;
15
11
  checkIntervalSeconds;
16
12
  cacheFile;
17
- encryptKey;
18
13
  deviceId;
14
+ serverUrl;
19
15
  softwareName;
20
- cacheValidityDays;
16
+ cacheDir;
21
17
  constructor(args) {
22
18
  this.deviceId = args.deviceId;
19
+ this.serverUrl = (0, stateBundle_1.normalizeServerUrl)(args.serverUrl);
23
20
  this.softwareName = args.softwareName;
24
- this.cacheValidityDays = args.cacheValidityDays;
25
- this.cacheValiditySeconds = args.cacheValidityDays * 24 * 60 * 60;
26
- this.checkIntervalSeconds = args.checkIntervalDays * 24 * 60 * 60;
27
- const cacheDir = args.cacheDir ?? defaultCacheDir();
21
+ const cacheValidityDays = args.cacheValidityDays > 0 ? args.cacheValidityDays : 7;
22
+ this.cacheValiditySeconds = cacheValidityDays * 24 * 60 * 60;
23
+ this.checkIntervalSeconds = (args.checkIntervalDays > 0 ? args.checkIntervalDays : 2) * 24 * 60 * 60;
24
+ this.cacheDir = args.storageRoot;
25
+ this.cacheFile = (0, stateBundle_1.bundlePath)(this.serverUrl, this.cacheDir);
26
+ }
27
+ readRow() {
28
+ const d = (0, stateBundle_1.readStateDict)(this.serverUrl, this.cacheDir);
29
+ if (!d)
30
+ return null;
31
+ const row = (0, stateBundle_1.loadAppsMap)(d)[this.softwareName];
32
+ return row && typeof row === "object" && !Array.isArray(row) ? row : null;
33
+ }
34
+ cacheRecordFromRow(row) {
35
+ if (!row)
36
+ return null;
37
+ const ts = (0, stateBundle_1.rowLastSuccessTs)(row);
38
+ if (ts === null)
39
+ return null;
40
+ return {
41
+ authorized: true,
42
+ message: "设备已授权",
43
+ cachedAt: ts,
44
+ };
45
+ }
46
+ snapshotForAuthorizationCheck() {
28
47
  try {
29
- node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
48
+ const row = this.readRow();
49
+ if (!row || (0, stateBundle_1.rowLastSuccessTs)(row) === null)
50
+ return { cacheData: null, storedHeartbeatTimes: 0 };
51
+ const raw = row.heartbeat_times;
52
+ const n = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : Number(raw);
53
+ if (!Number.isFinite(n) || n < 1) {
54
+ this.clearCache();
55
+ return { cacheData: null, storedHeartbeatTimes: 0 };
56
+ }
57
+ return { cacheData: this.cacheRecordFromRow(row), storedHeartbeatTimes: n };
30
58
  }
31
59
  catch {
32
- // ignore
60
+ return { cacheData: null, storedHeartbeatTimes: 0 };
33
61
  }
34
- const cacheKey = `${args.deviceId}:${args.softwareName}`;
35
- const fileHash = node_crypto_1.default.createHash("md5").update(cacheKey, "utf8").digest("hex").slice(0, 12);
36
- this.cacheFile = node_path_1.default.join(cacheDir, `runtime_${fileHash}.dat`);
37
- const material = `${args.serverUrl}:${args.deviceId}:${args.softwareName}:obfuscate_v1`;
38
- this.encryptKey = node_crypto_1.default.createHash("sha256").update(material, "utf8").digest();
62
+ }
63
+ isCacheTTLValid(cache) {
64
+ if (!cache?.cachedAt)
65
+ return false;
66
+ const elapsed = Date.now() / 1000 - cache.cachedAt;
67
+ return elapsed < this.cacheValiditySeconds;
68
+ }
69
+ needsCheck() {
70
+ const cache = this.getCache();
71
+ if (!cache?.cachedAt)
72
+ return true;
73
+ const elapsed = Date.now() / 1000 - cache.cachedAt;
74
+ return elapsed >= this.checkIntervalSeconds;
39
75
  }
40
76
  getCache() {
41
77
  try {
42
- if (!node_fs_1.default.existsSync(this.cacheFile))
43
- return null;
44
- const encrypted = node_fs_1.default.readFileSync(this.cacheFile);
45
- const decrypted = this.deobfuscate(encrypted);
46
- if (!decrypted)
47
- return null;
48
- const raw = JSON.parse(decrypted.toString("utf8"));
49
- return {
50
- authorized: !!raw.a,
51
- message: raw.m ?? "",
52
- cachedAt: Number(raw.c ?? 0),
53
- lastCheck: Number(raw.l ?? 0),
54
- };
78
+ return this.cacheRecordFromRow(this.readRow());
55
79
  }
56
80
  catch {
57
81
  return null;
58
82
  }
59
83
  }
60
- saveCache(authorized, message) {
84
+ loadDeviceInfoSnapshot() {
61
85
  try {
62
- const now = Date.now() / 1000;
63
- const timeStr = formatPythonTimeString(now);
64
- const fake = node_crypto_1.default.createHash("md5").update(timeStr, "utf8").digest("hex").slice(0, 8);
65
- const payload = {
66
- a: authorized,
67
- m: message,
68
- c: now,
69
- l: now,
70
- v: 2,
71
- f: fake,
72
- };
73
- const json = JSON.stringify(payload);
74
- const encrypted = this.obfuscate(Buffer.from(json, "utf8"));
75
- if (!encrypted)
76
- return false;
77
- try {
78
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.cacheFile), { recursive: true });
79
- }
80
- catch {
81
- // ignore
82
- }
83
- try {
84
- node_fs_1.default.writeFileSync(this.cacheFile, encrypted);
85
- }
86
- catch {
86
+ const row = this.readRow();
87
+ if (!row)
88
+ return null;
89
+ const raw = row[stateBundle_1.BUNDLE_PRODUCT_DEVICE_INFO_SNAPSHOT_KEY];
90
+ if (raw == null)
91
+ return null;
92
+ if (typeof raw === "string") {
93
+ if (!raw.trim())
94
+ return null;
87
95
  try {
88
- if (node_fs_1.default.existsSync(this.cacheFile))
89
- node_fs_1.default.unlinkSync(this.cacheFile);
90
- node_fs_1.default.writeFileSync(this.cacheFile, encrypted);
96
+ const p = JSON.parse(raw);
97
+ if (!p || typeof p !== "object" || Array.isArray(p))
98
+ return null;
99
+ return JSON.parse(JSON.stringify(p));
91
100
  }
92
101
  catch {
93
- return false;
102
+ return null;
94
103
  }
95
104
  }
96
- if (process.platform === "win32") {
97
- try {
98
- (0, node_child_process_1.execFileSync)("attrib", ["+H", this.cacheFile], { stdio: "ignore" });
99
- }
100
- catch {
101
- // ignore
102
- }
105
+ if (typeof raw === "object" && !Array.isArray(raw)) {
106
+ return JSON.parse(JSON.stringify(raw));
103
107
  }
104
- return true;
108
+ return null;
105
109
  }
106
110
  catch {
107
- return false;
111
+ return null;
108
112
  }
109
113
  }
110
- isCacheValid() {
111
- const cache = this.getCache();
112
- if (!cache)
113
- return false;
114
- const elapsed = Date.now() / 1000 - cache.cachedAt;
115
- return elapsed < this.cacheValiditySeconds;
116
- }
117
- clearCache() {
114
+ saveCache(authorized, _message, nextHeartbeat, deviceInfoSnapshot) {
118
115
  try {
119
- if (node_fs_1.default.existsSync(this.cacheFile))
120
- node_fs_1.default.unlinkSync(this.cacheFile);
121
- return true;
116
+ const now = Date.now() / 1000;
117
+ const cur = (0, stateBundle_1.readStateDict)(this.serverUrl, this.cacheDir) ?? {};
118
+ const payload = { ...cur };
119
+ for (const k of stateBundle_1.BUNDLE_ROOT_STRAY_KEYS)
120
+ delete payload[k];
121
+ const apps = (0, stateBundle_1.loadAppsMap)(payload);
122
+ const sub = { ...(apps[this.softwareName] ?? {}) };
123
+ delete sub.software_name;
124
+ if (!authorized) {
125
+ for (const k of stateBundle_1.BUNDLE_PRODUCT_REVOKE_KEYS)
126
+ delete sub[k];
127
+ sub.device_id = this.deviceId;
128
+ }
129
+ else {
130
+ sub.device_id = this.deviceId;
131
+ sub.last_success_at = now;
132
+ if (nextHeartbeat !== undefined)
133
+ sub.heartbeat_times = nextHeartbeat;
134
+ if (deviceInfoSnapshot !== undefined) {
135
+ sub[stateBundle_1.BUNDLE_PRODUCT_DEVICE_INFO_SNAPSHOT_KEY] = JSON.parse(JSON.stringify(deviceInfoSnapshot));
136
+ }
137
+ }
138
+ apps[this.softwareName] = sub;
139
+ (0, stateBundle_1.commitAppsMap)(payload, apps);
140
+ return (0, stateBundle_1.writeStateDictWithRetry)(this.serverUrl, payload, this.cacheDir);
122
141
  }
123
142
  catch {
124
143
  return false;
125
144
  }
126
145
  }
127
- obfuscate(data) {
146
+ clearCache() {
128
147
  try {
129
- const compressed = node_zlib_1.default.deflateSync(data, { level: 9 });
130
- const xored = xorWithKey(compressed, this.encryptKey);
131
- const timeSeed = Math.floor(Date.now() / 1000 / 3600);
132
- const prefixSeed = node_crypto_1.default
133
- .createHash("md5")
134
- .update(`${this.deviceId}:${this.softwareName}:${timeSeed}`, "utf8")
135
- .digest()
136
- .subarray(0, 4);
137
- const lenBuf = Buffer.alloc(4);
138
- lenBuf.writeUInt32BE(xored.length, 0);
139
- const packed = Buffer.concat([prefixSeed, lenBuf, xored]);
140
- const finalKey = node_crypto_1.default.createHash("sha256").update(Buffer.concat([this.encryptKey, prefixSeed])).digest();
141
- return xorWithKey(packed, finalKey);
148
+ const d = (0, stateBundle_1.readStateDict)(this.serverUrl, this.cacheDir);
149
+ if (!d)
150
+ return true;
151
+ const next = { ...d };
152
+ for (const k of stateBundle_1.BUNDLE_ROOT_STRAY_KEYS)
153
+ delete next[k];
154
+ const apps = (0, stateBundle_1.loadAppsMap)(next);
155
+ const sub = { ...(apps[this.softwareName] ?? {}) };
156
+ for (const k of stateBundle_1.BUNDLE_PRODUCT_REVOKE_KEYS)
157
+ delete sub[k];
158
+ delete sub.software_name;
159
+ sub.device_id = this.deviceId;
160
+ apps[this.softwareName] = sub;
161
+ (0, stateBundle_1.commitAppsMap)(next, apps);
162
+ const anyAppHasDevice = Object.values(apps).some((r) => {
163
+ if (!r || typeof r !== "object" || Array.isArray(r))
164
+ return false;
165
+ return (0, stateBundle_1.rowDeviceId)(r) != null;
166
+ });
167
+ if (!anyAppHasDevice) {
168
+ try {
169
+ if (node_fs_1.default.existsSync(this.cacheFile))
170
+ node_fs_1.default.unlinkSync(this.cacheFile);
171
+ }
172
+ catch { }
173
+ return true;
174
+ }
175
+ return (0, stateBundle_1.writeStateDictWithRetry)(this.serverUrl, next, this.cacheDir);
142
176
  }
143
177
  catch {
144
- return null;
145
- }
146
- }
147
- deobfuscate(data) {
148
- if (data.length < 8)
149
- return null;
150
- const currentHour = Math.floor(Date.now() / 1000 / 3600);
151
- const maxOffset = Math.max(2, this.cacheValidityDays * 24 + 12);
152
- for (let hourOffset = -maxOffset; hourOffset <= maxOffset; hourOffset++) {
153
- const timeSeed = currentHour + hourOffset;
154
- const prefixSeed = node_crypto_1.default
155
- .createHash("md5")
156
- .update(`${this.deviceId}:${this.softwareName}:${timeSeed}`, "utf8")
157
- .digest()
158
- .subarray(0, 4);
159
- const finalKey = node_crypto_1.default.createHash("sha256").update(Buffer.concat([this.encryptKey, prefixSeed])).digest();
160
- const unpacked = xorWithKey(data, finalKey);
161
- if (!unpacked.subarray(0, 4).equals(prefixSeed))
162
- continue;
163
- const length = unpacked.readUInt32BE(4);
164
- if (length > unpacked.length - 8)
165
- continue;
166
- const xored = unpacked.subarray(8, 8 + length);
167
- const compressed = xorWithKey(xored, this.encryptKey);
168
- try {
169
- return node_zlib_1.default.inflateSync(compressed);
170
- }
171
- catch {
172
- continue;
173
- }
178
+ return false;
174
179
  }
175
- return null;
176
180
  }
177
181
  }
178
182
  exports.AuthCache = AuthCache;
179
- function xorWithKey(data, key) {
180
- const out = Buffer.allocUnsafe(data.length);
181
- for (let i = 0; i < data.length; i++) {
182
- out[i] = data[i] ^ key[i % key.length];
183
- }
184
- return out;
185
- }
186
- function defaultCacheDir() {
187
- const home = node_os_1.default.homedir();
188
- if (process.platform === "win32") {
189
- const local = process.env.LOCALAPPDATA ?? node_path_1.default.join(home, "AppData", "Local");
190
- return node_path_1.default.join(local, "Microsoft", "CLR_v4.0");
191
- }
192
- if (process.platform === "darwin") {
193
- return node_path_1.default.join(home, "Library", "Caches", ".com.apple.metadata");
194
- }
195
- return node_path_1.default.join(home, ".cache", ".fontconfig");
196
- }
197
- function formatPythonTimeString(nowSeconds) {
198
- // 目标:模拟 Python str(time.time()) 的大致风格(最短表示,必要时带 .0)
199
- // 这里按 Go 实现保持一致:g/最短,但确保有小数点或 e
200
- let s = String(nowSeconds);
201
- if (!s.includes(".") && !s.includes("e"))
202
- s += ".0";
203
- return s;
204
- }
package/dist/client.d.ts CHANGED
@@ -4,16 +4,28 @@ export declare class AuthClient {
4
4
  private readonly softwareName;
5
5
  private readonly softwareVersion;
6
6
  private readonly deviceId;
7
- private readonly deviceInfo;
7
+ private deviceInfo;
8
+ private deviceInfoDeferred;
8
9
  private readonly clientSecret;
9
10
  private readonly debug;
10
- private readonly cache?;
11
+ private readonly cache;
12
+ private readonly stateBundleExistedBeforeInit;
11
13
  constructor(config: AuthClientConfig);
12
14
  private logDebug;
15
+ private ensureFullDeviceInfo;
13
16
  private formatRemainingTime;
17
+ private postHeartbeatDeviceInfo;
18
+ private checkOnlineLight;
14
19
  private checkOnline;
15
- checkAuthorization(): Promise<AuthResult>;
16
- requireAuthorization(): Promise<boolean>;
20
+ private persistOnlineResult;
21
+ checkAuthorization(_forceOnline?: boolean): Promise<AuthResult>;
22
+ checkAuthorizationProgressive(_forceOnline?: boolean): Promise<AuthResult>;
23
+ requireAuthorization(forceOnline?: boolean): Promise<boolean>;
17
24
  clearCache(): boolean;
25
+ canSoftLaunch(): boolean;
26
+ startBackgroundRefresh(options?: {
27
+ forceOnline?: boolean;
28
+ onDone?: (result: AuthResult) => void;
29
+ }): boolean;
18
30
  getAuthorizationInfo(): Promise<AuthorizationInfo>;
19
31
  }