py-auth-client 0.1.4 → 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/dist/client.js CHANGED
@@ -1,20 +1,35 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.AuthClient = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_os_1 = __importDefault(require("node:os"));
4
9
  const errors_1 = require("./errors");
5
10
  const crypto_1 = require("./crypto");
6
11
  const cache_1 = require("./cache");
7
12
  const device_1 = require("./device");
8
13
  const http_1 = require("./http");
14
+ const storage_1 = require("./storage");
15
+ const stateBundle_1 = require("./stateBundle");
16
+ const sdkMeta_1 = require("./sdkMeta");
17
+ const SECONDS_PER_MINUTE = 60;
18
+ const SECONDS_PER_HOUR = 3600;
19
+ const SECONDS_PER_DAY = 86400;
20
+ const HEARTBEAT_TIMEOUT_MS = 2_000;
21
+ const HEARTBEAT_LIGHT_TIMEOUT_MS = 4_000;
9
22
  class AuthClient {
10
23
  serverUrl;
11
24
  softwareName;
12
25
  softwareVersion;
13
26
  deviceId;
14
27
  deviceInfo;
28
+ deviceInfoDeferred;
15
29
  clientSecret;
16
30
  debug;
17
31
  cache;
32
+ stateBundleExistedBeforeInit;
18
33
  constructor(config) {
19
34
  if (!config.serverUrl)
20
35
  throw new Error("serverUrl不能为空");
@@ -29,43 +44,82 @@ class AuthClient {
29
44
  this.softwareVersion = config.softwareVersion ?? "0.0.0";
30
45
  this.clientSecret = secret;
31
46
  this.debug = !!config.debug;
32
- this.deviceId = (0, device_1.buildDeviceId)(this.serverUrl, config.deviceId, this.softwareName);
33
- this.deviceInfo = {
34
- ...(config.deviceInfo ?? (0, device_1.collectDeviceInfo)()),
35
- software_version: this.softwareVersion,
36
- };
37
- const enableCache = config.enableCache ?? true;
38
47
  const cacheValidityDays = config.cacheValidityDays ?? 7;
39
48
  const checkIntervalDays = config.checkIntervalDays ?? 2;
40
- if (enableCache) {
41
- this.cache = new cache_1.AuthCache({
42
- cacheDir: config.cacheDir,
43
- deviceId: this.deviceId,
44
- serverUrl: this.serverUrl,
45
- softwareName: this.softwareName,
46
- cacheValidityDays,
47
- checkIntervalDays,
48
- });
49
+ const storageBase = (0, storage_1.getClientStorageRoot)();
50
+ const stateBundleExistedBeforeInit = node_fs_1.default.existsSync((0, stateBundle_1.bundlePath)(this.serverUrl, storageBase));
51
+ const hasStableDeviceId = !!config.deviceId || !!(0, device_1.loadPersistedDeviceId)(this.serverUrl, this.softwareName, storageBase);
52
+ this.deviceId = (0, device_1.buildDeviceId)(this.serverUrl, config.deviceId, this.softwareName, storageBase);
53
+ if (config.deviceInfo) {
54
+ this.deviceInfoDeferred = false;
55
+ this.deviceInfo = {
56
+ ...config.deviceInfo,
57
+ software_version: this.softwareVersion,
58
+ sdk: (0, sdkMeta_1.baseSdk)(process.version),
59
+ };
60
+ }
61
+ else if (hasStableDeviceId) {
62
+ this.deviceInfoDeferred = true;
63
+ this.deviceInfo = {
64
+ software_version: this.softwareVersion,
65
+ sdk: (0, sdkMeta_1.baseSdk)(process.version),
66
+ };
67
+ }
68
+ else {
69
+ this.deviceInfoDeferred = false;
70
+ this.deviceInfo = {
71
+ ...(0, device_1.collectDeviceInfo)(),
72
+ software_version: this.softwareVersion,
73
+ sdk: (0, sdkMeta_1.baseSdk)(process.version),
74
+ };
49
75
  }
76
+ this.cache = new cache_1.AuthCache({
77
+ storageRoot: storageBase,
78
+ deviceId: this.deviceId,
79
+ serverUrl: this.serverUrl,
80
+ softwareName: this.softwareName,
81
+ cacheValidityDays,
82
+ checkIntervalDays,
83
+ });
84
+ if (this.deviceInfoDeferred) {
85
+ const snap = this.cache.loadDeviceInfoSnapshot();
86
+ if (snap && typeof snap === "object") {
87
+ this.deviceInfo = {
88
+ ...snap,
89
+ software_version: this.softwareVersion,
90
+ sdk: (0, sdkMeta_1.baseSdk)(process.version),
91
+ };
92
+ this.deviceInfoDeferred = false;
93
+ }
94
+ }
95
+ this.stateBundleExistedBeforeInit = stateBundleExistedBeforeInit;
50
96
  }
51
97
  logDebug(msg) {
52
98
  if (this.debug) {
53
- // 不加额外依赖,直接输出
54
- // eslint-disable-next-line no-console
55
- console.debug(`[ts-auth-client][DEBUG] ${msg}`);
99
+ console.debug(`[ts][DEBUG] ${msg}`);
56
100
  }
57
101
  }
102
+ ensureFullDeviceInfo() {
103
+ if (!this.deviceInfoDeferred)
104
+ return;
105
+ this.deviceInfo = {
106
+ ...(0, device_1.collectDeviceInfo)(),
107
+ software_version: this.softwareVersion,
108
+ sdk: (0, sdkMeta_1.baseSdk)(process.version),
109
+ };
110
+ this.deviceInfoDeferred = false;
111
+ }
58
112
  formatRemainingTime(cachedAtSeconds) {
59
- if (!cachedAtSeconds || cachedAtSeconds <= 0 || !this.cache)
113
+ if (!cachedAtSeconds || cachedAtSeconds <= 0)
60
114
  return "未知";
61
115
  const now = Date.now() / 1000;
62
116
  const elapsed = now - cachedAtSeconds;
63
117
  const remaining = this.cache.cacheValiditySeconds - elapsed;
64
118
  if (remaining <= 0)
65
119
  return "已过期";
66
- const days = Math.floor(remaining / 86400);
67
- const hours = Math.floor((remaining % 86400) / 3600);
68
- const minutes = Math.floor((remaining % 3600) / 60);
120
+ const days = Math.floor(remaining / SECONDS_PER_DAY);
121
+ const hours = Math.floor((remaining % SECONDS_PER_DAY) / SECONDS_PER_HOUR);
122
+ const minutes = Math.floor((remaining % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
69
123
  const parts = [];
70
124
  if (days > 0)
71
125
  parts.push(`${days}天`);
@@ -75,37 +129,55 @@ class AuthClient {
75
129
  parts.push(`${minutes}分钟`);
76
130
  return parts.join("");
77
131
  }
78
- async checkOnline() {
132
+ async postHeartbeatDeviceInfo(deviceInfoPayload, heartbeatTimes, timeoutMs) {
133
+ const device_info = {
134
+ ...deviceInfoPayload,
135
+ sdk: {
136
+ ...(deviceInfoPayload.sdk ?? (0, sdkMeta_1.baseSdk)(process.version)),
137
+ heartbeat_times: heartbeatTimes,
138
+ },
139
+ };
140
+ const requestData = {
141
+ device_id: this.deviceId,
142
+ software_name: this.softwareName,
143
+ device_info,
144
+ };
145
+ const encrypted = (0, crypto_1.encryptData)(JSON.stringify(requestData), this.clientSecret);
146
+ const { status, json, text } = await (0, http_1.postJson)(`${this.serverUrl}/api/auth/heartbeat`, { encrypted_data: encrypted }, timeoutMs);
147
+ if (status === 200) {
148
+ const token = json?.encrypted_data ?? "";
149
+ const decryptedText = token ? (0, crypto_1.decryptData)(token, this.clientSecret) : null;
150
+ if (!decryptedText) {
151
+ this.logDebug("在线订阅响应解密失败");
152
+ return { authorized: false, message: "解密响应失败", success: false, from_cache: false };
153
+ }
154
+ const decrypted = JSON.parse(decryptedText);
155
+ this.logDebug(`在线订阅成功,authorized=${!!decrypted.authorized}`);
156
+ return {
157
+ authorized: !!decrypted.authorized,
158
+ message: decrypted.message ?? "",
159
+ success: true,
160
+ from_cache: false,
161
+ };
162
+ }
163
+ const detail = json?.detail;
164
+ const msg = status === 403 && typeof detail === "string" ? detail : `服务器错误: ${status}`;
165
+ this.logDebug(`在线订阅失败,status=${status}, message=${msg}; raw=${text}`);
166
+ return { authorized: false, message: msg, success: false, from_cache: false, is_auth_error: status === 403 };
167
+ }
168
+ async checkOnlineLight(heartbeatTimes) {
79
169
  try {
80
- this.logDebug("开始在线订阅请求...");
81
- const requestData = {
82
- device_id: this.deviceId,
83
- software_name: this.softwareName,
84
- device_info: this.deviceInfo,
170
+ this.logDebug("轻量在线订阅请求(不等待全量 device_info / 公网 IP)...");
171
+ const light = {
172
+ ...this.deviceInfo,
173
+ software_version: this.softwareVersion,
174
+ system: {
175
+ ...(this.deviceInfo.system ?? {}),
176
+ hostname: this.deviceInfo.system?.hostname?.trim() || node_os_1.default.hostname(),
177
+ os: this.deviceInfo.system?.os || process.platform,
178
+ },
85
179
  };
86
- const encrypted = (0, crypto_1.encryptData)(JSON.stringify(requestData), this.clientSecret);
87
- const { status, json, text } = await (0, http_1.postJson)(`${this.serverUrl}/api/auth/heartbeat`, { encrypted_data: encrypted }, 10_000);
88
- if (status === 200) {
89
- const token = json?.encrypted_data ?? "";
90
- const decryptedText = token ? (0, crypto_1.decryptData)(token, this.clientSecret) : null;
91
- if (!decryptedText) {
92
- this.logDebug("在线订阅响应解密失败");
93
- return { authorized: false, message: "解密响应失败", success: false, from_cache: false };
94
- }
95
- const decrypted = JSON.parse(decryptedText);
96
- this.logDebug(`在线订阅成功,authorized=${!!decrypted.authorized}`);
97
- return {
98
- authorized: !!decrypted.authorized,
99
- message: decrypted.message ?? "",
100
- success: true,
101
- from_cache: false,
102
- };
103
- }
104
- // Python 逻辑:403 取 detail,其它返回 “服务器错误: status”
105
- const detail = json?.detail;
106
- const msg = status === 403 && typeof detail === "string" ? detail : `服务器错误: ${status}`;
107
- this.logDebug(`在线订阅失败,status=${status}, message=${msg}; raw=${text}`);
108
- return { authorized: false, message: msg, success: false, from_cache: false, is_auth_error: status === 403 };
180
+ return await this.postHeartbeatDeviceInfo(light, heartbeatTimes, HEARTBEAT_LIGHT_TIMEOUT_MS);
109
181
  }
110
182
  catch (e) {
111
183
  const msg = e?.name === "AbortError" ? "连接失败: timeout" : `连接失败: ${String(e?.message ?? e)}`;
@@ -113,29 +185,68 @@ class AuthClient {
113
185
  return { authorized: false, message: msg, success: false, from_cache: false };
114
186
  }
115
187
  }
116
- async checkAuthorization() {
117
- if (!this.cache) {
118
- return this.checkOnline();
119
- }
120
- let cacheData = this.cache.getCache();
121
- let cacheValid = false;
122
- if (cacheData?.cachedAt) {
123
- const elapsed = Date.now() / 1000 - cacheData.cachedAt;
124
- if (elapsed < this.cache.cacheValiditySeconds) {
125
- cacheValid = true;
126
- this.logDebug("命中有效缓存,直接授权通过");
188
+ async checkOnline(heartbeatTimes) {
189
+ try {
190
+ const hasPublicIp = typeof this.deviceInfo.network?.public_ip === "string" &&
191
+ this.deviceInfo.network.public_ip.trim() !== "";
192
+ const needPublicIp = this.deviceInfoDeferred || !hasPublicIp;
193
+ const pubPromise = needPublicIp ? (0, device_1.fetchPublicIp)() : Promise.resolve("");
194
+ this.ensureFullDeviceInfo();
195
+ this.logDebug("开始在线订阅请求...");
196
+ const pub = await pubPromise;
197
+ const nw = { ...(this.deviceInfo.network ?? {}) };
198
+ if (pub)
199
+ nw.public_ip = pub;
200
+ if (pub) {
201
+ this.deviceInfo = { ...this.deviceInfo, network: nw };
127
202
  }
203
+ const device_info = {
204
+ ...this.deviceInfo,
205
+ ...(Object.keys(nw).length > 0 ? { network: nw } : {}),
206
+ };
207
+ return await this.postHeartbeatDeviceInfo(device_info, heartbeatTimes, HEARTBEAT_TIMEOUT_MS);
208
+ }
209
+ catch (e) {
210
+ const msg = e?.name === "AbortError" ? "连接失败: timeout" : `连接失败: ${String(e?.message ?? e)}`;
211
+ this.logDebug(`在线订阅请求异常: ${msg}`);
212
+ return { authorized: false, message: msg, success: false, from_cache: false };
213
+ }
214
+ }
215
+ persistOnlineResult(result, heartbeatIfAuthorized) {
216
+ const snap = result.authorized
217
+ ? JSON.parse(JSON.stringify(this.deviceInfo))
218
+ : undefined;
219
+ this.cache.saveCache(result.authorized, result.message, result.authorized ? heartbeatIfAuthorized : undefined, snap);
220
+ }
221
+ async checkAuthorization(_forceOnline = false) {
222
+ if (this.debug) {
223
+ const cf = this.cache.cacheFile;
224
+ const fileNow = node_fs_1.default.existsSync(cf);
225
+ const pre = this.stateBundleExistedBeforeInit;
226
+ let desc = "不存在(持久化可能失败)";
227
+ if (fileNow && pre)
228
+ desc = "启动前已存在";
229
+ else if (fileNow && !pre)
230
+ desc = "启动前不存在,构造客户端时已新建(device_id 持久化)";
231
+ else if (!fileNow && pre)
232
+ desc = "启动前曾有,当前缺失(异常)";
233
+ this.logDebug(`状态包: ${cf} | ${desc}`);
128
234
  }
235
+ const snap = this.cache.snapshotForAuthorizationCheck();
236
+ const cacheData = snap.cacheData;
237
+ const cacheValid = this.cache.isCacheTTLValid(cacheData);
129
238
  if (cacheValid) {
239
+ this.logDebug("本地缓存仍在有效期内(在线失败时可作后备)");
130
240
  this.logDebug("缓存有效,继续尝试在线订阅来更新订阅");
131
241
  }
132
242
  else {
133
243
  this.logDebug(cacheData ? "缓存存在但已过期,准备发起在线订阅请求" : "未找到缓存,准备发起在线订阅请求");
134
244
  }
135
- const onlineResult = await this.checkOnline();
245
+ const nextHb = snap.storedHeartbeatTimes + 1;
246
+ const onlineResult = await this.checkOnline(nextHb);
136
247
  if (onlineResult.success) {
137
248
  this.logDebug("在线订阅成功,更新缓存");
138
- this.cache.saveCache(onlineResult.authorized, onlineResult.message);
249
+ this.persistOnlineResult(onlineResult, nextHb);
139
250
  return onlineResult;
140
251
  }
141
252
  if (cacheValid && cacheData) {
@@ -150,17 +261,80 @@ class AuthClient {
150
261
  }
151
262
  return onlineResult;
152
263
  }
153
- async requireAuthorization() {
154
- const result = await this.checkAuthorization();
155
- if (!result.success) {
156
- throw new errors_1.AuthorizationError({
157
- message: result.message,
158
- result,
159
- deviceId: this.deviceId,
160
- serverUrl: this.serverUrl,
161
- });
264
+ async checkAuthorizationProgressive(_forceOnline = false) {
265
+ if (this.debug) {
266
+ const cf = this.cache.cacheFile;
267
+ const fileNow = node_fs_1.default.existsSync(cf);
268
+ const pre = this.stateBundleExistedBeforeInit;
269
+ let desc = "不存在(持久化可能失败)";
270
+ if (fileNow && pre)
271
+ desc = "启动前已存在";
272
+ else if (fileNow && !pre)
273
+ desc = "启动前不存在,构造客户端时已新建(device_id 持久化)";
274
+ else if (!fileNow && pre)
275
+ desc = "启动前曾有,当前缺失(异常)";
276
+ this.logDebug(`状态包: ${cf} | ${desc}`);
277
+ }
278
+ const snap = this.cache.snapshotForAuthorizationCheck();
279
+ const cacheData = snap.cacheData;
280
+ const cacheValid = this.cache.isCacheTTLValid(cacheData);
281
+ if (cacheValid) {
282
+ this.logDebug("本地缓存仍在有效期内(在线失败时可作后备)");
283
+ this.logDebug("缓存有效,继续尝试在线订阅来更新订阅");
162
284
  }
163
- if (!result.authorized) {
285
+ else {
286
+ this.logDebug(cacheData ? "缓存存在但已过期,准备发起在线订阅请求" : "未找到缓存,准备发起在线订阅请求");
287
+ }
288
+ const nextHb = snap.storedHeartbeatTimes + 1;
289
+ const rFast = await this.checkOnlineLight(nextHb);
290
+ if (rFast.success) {
291
+ if (rFast.authorized) {
292
+ this.logDebug("在线订阅成功,更新缓存");
293
+ this.persistOnlineResult(rFast, nextHb);
294
+ this.logDebug("轻量心跳已落盘,发起全量 device_info 补全心跳...");
295
+ const rFull = await this.checkOnline(nextHb + 1);
296
+ if (rFull.success) {
297
+ this.logDebug("在线订阅成功,更新缓存");
298
+ this.persistOnlineResult(rFull, rFull.authorized ? nextHb + 1 : undefined);
299
+ return rFull;
300
+ }
301
+ const gc = this.cache.getCache();
302
+ if (gc && this.cache.isCacheTTLValid(gc)) {
303
+ const remaining = this.formatRemainingTime(gc.cachedAt);
304
+ this.logDebug(`补全心跳失败,沿用轻量结果,订阅剩余时间: ${remaining}`);
305
+ return {
306
+ authorized: gc.authorized,
307
+ message: gc.message,
308
+ success: true,
309
+ from_cache: true,
310
+ };
311
+ }
312
+ return rFull;
313
+ }
314
+ this.persistOnlineResult(rFast, undefined);
315
+ return rFast;
316
+ }
317
+ const onlineResult = await this.checkOnline(nextHb);
318
+ if (onlineResult.success) {
319
+ this.logDebug("在线订阅成功,更新缓存");
320
+ this.persistOnlineResult(onlineResult, onlineResult.authorized ? nextHb : undefined);
321
+ return onlineResult;
322
+ }
323
+ if (cacheValid && cacheData) {
324
+ const remaining = this.formatRemainingTime(cacheData.cachedAt);
325
+ this.logDebug(`在线订阅失败,但缓存有效,使用缓存结果,订阅剩余时间: ${remaining}`);
326
+ return {
327
+ authorized: cacheData.authorized,
328
+ message: cacheData.message,
329
+ success: true,
330
+ from_cache: true,
331
+ };
332
+ }
333
+ return onlineResult;
334
+ }
335
+ async requireAuthorization(forceOnline = false) {
336
+ const result = await this.checkAuthorization(forceOnline);
337
+ if (!result.success || !result.authorized) {
164
338
  throw new errors_1.AuthorizationError({
165
339
  message: result.message,
166
340
  result,
@@ -171,35 +345,59 @@ class AuthClient {
171
345
  return true;
172
346
  }
173
347
  clearCache() {
174
- if (!this.cache)
175
- return true;
176
348
  return this.cache.clearCache();
177
349
  }
350
+ canSoftLaunch() {
351
+ const c = this.cache.getCache();
352
+ return !!(c?.authorized && this.cache.isCacheTTLValid(c));
353
+ }
354
+ startBackgroundRefresh(options) {
355
+ const soft = this.canSoftLaunch();
356
+ const fo = options?.forceOnline ?? false;
357
+ void this.checkAuthorizationProgressive(fo).then((r) => {
358
+ options?.onDone?.(r);
359
+ }, (err) => {
360
+ options?.onDone?.({
361
+ authorized: false,
362
+ success: false,
363
+ from_cache: false,
364
+ message: err instanceof Error ? err.message : String(err),
365
+ });
366
+ });
367
+ return soft;
368
+ }
178
369
  async getAuthorizationInfo() {
179
- const result = await this.checkAuthorization();
180
- const info = {
181
- authorized: result.authorized,
182
- success: result.success,
183
- from_cache: result.from_cache,
184
- message: result.message,
185
- device_id: this.deviceId,
186
- server_url: this.serverUrl,
187
- };
188
- if (this.cache) {
189
- const cache = this.cache.getCache();
190
- if (cache) {
191
- const remaining = this.formatRemainingTime(cache.cachedAt);
192
- info.remaining_time = remaining;
193
- info.cache_valid = this.cache.isCacheValid();
194
- info.cached_at = cache.cachedAt;
195
- if (cache.cachedAt > 0) {
196
- info.cached_at_readable = new Date(cache.cachedAt * 1000).toISOString().replace("T", " ").slice(0, 19);
197
- }
370
+ const cache = this.cache.getCache();
371
+ const info = cache
372
+ ? {
373
+ authorized: cache.authorized,
374
+ success: true,
375
+ from_cache: true,
376
+ message: cache.message,
377
+ device_id: this.deviceId,
378
+ server_url: this.serverUrl,
379
+ remaining_time: this.formatRemainingTime(cache.cachedAt),
380
+ cache_valid: this.cache.isCacheTTLValid(cache),
381
+ cached_at: cache.cachedAt,
382
+ cached_at_readable: cache.cachedAt > 0
383
+ ? new Date(cache.cachedAt * 1000).toISOString().replace("T", " ").slice(0, 19)
384
+ : undefined,
198
385
  }
199
- else {
200
- info.remaining_time = "无缓存";
201
- info.cache_valid = false;
386
+ : {
387
+ authorized: false,
388
+ success: false,
389
+ from_cache: false,
390
+ message: "无本地授权缓存",
391
+ device_id: this.deviceId,
392
+ server_url: this.serverUrl,
393
+ remaining_time: "无缓存",
394
+ cache_valid: false,
395
+ };
396
+ if (this.debug) {
397
+ try {
398
+ this.logDebug(`授权信息摘要:\n${JSON.stringify(info, null, 2)}`);
202
399
  }
400
+ catch { }
203
401
  }
204
402
  return info;
205
403
  }
package/dist/device.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DeviceInfo } from "./types";
2
- export declare function loadPersistedDeviceId(serverUrl: string, softwareName: string): string | null;
3
- export declare function persistDeviceId(serverUrl: string, softwareName: string, deviceId: string): void;
4
- export declare function buildDeviceId(serverUrl: string, providedDeviceId: string | undefined, softwareName: string): string;
2
+ export declare function loadPersistedDeviceId(serverUrl: string, softwareName: string, baseDir: string): string | null;
3
+ export declare function buildDeviceId(serverUrl: string, providedDeviceId: string | undefined, softwareName: string, baseDir: string): string;
5
4
  export declare function collectDeviceInfo(): DeviceInfo;
5
+ export declare function fetchPublicIp(): Promise<string>;