izteamslots 1.5.0 → 1.5.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.5.2](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.1...v1.5.2) (2026-03-06)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * security and thread-safety improvements from code review ([6356bfd](https://github.com/izzzzzi/izTeamSlots/commit/6356bfd9eded9379fe1424b20f954cec8e847c8b))
7
+
8
+ ## [1.5.1](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.0...v1.5.1) (2026-03-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * use self.password_prefix in boomlify generate() instead of hardcoded string ([de0098a](https://github.com/izzzzzi/izTeamSlots/commit/de0098ad620ee3010c359155b726f097b15fb13f))
14
+
1
15
  # [1.5.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.4.0...v1.5.0) (2026-03-06)
2
16
 
3
17
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import shutil
5
+ import threading
5
6
  import uuid
6
7
  from dataclasses import dataclass
7
8
  from datetime import datetime, timezone
@@ -46,6 +47,7 @@ class AccountStore:
46
47
  self.base_dir = base_dir
47
48
  self.admin_dir = base_dir / "admin"
48
49
  self.worker_dir = base_dir / "worker"
50
+ self._lock = threading.Lock()
49
51
  self.admin_dir.mkdir(parents=True, exist_ok=True)
50
52
  self.worker_dir.mkdir(parents=True, exist_ok=True)
51
53
 
@@ -113,49 +115,52 @@ class AccountStore:
113
115
  )
114
116
 
115
117
  def add_admin(self, email: str, password: str) -> AdminAccount:
116
- index = self._read_index(self.admin_dir)
117
- if email in index:
118
- raise ValueError(f"Админ {email} уже существует")
119
- account_id = uuid.uuid4().hex
120
- now = datetime.now(timezone.utc).isoformat()
121
- index[email] = {"id": account_id, "created_at": now}
122
- self._write_index(self.admin_dir, index)
123
- account_dir = self.admin_dir / account_id
124
- self._write_meta(account_dir, {"email": email, "password": password})
125
- return AdminAccount(id=account_id, email=email, password=password, created_at=now)
118
+ with self._lock:
119
+ index = self._read_index(self.admin_dir)
120
+ if email in index:
121
+ raise ValueError(f"Админ {email} уже существует")
122
+ account_id = uuid.uuid4().hex
123
+ now = datetime.now(timezone.utc).isoformat()
124
+ index[email] = {"id": account_id, "created_at": now}
125
+ self._write_index(self.admin_dir, index)
126
+ account_dir = self.admin_dir / account_id
127
+ self._write_meta(account_dir, {"email": email, "password": password})
128
+ return AdminAccount(id=account_id, email=email, password=password, created_at=now)
126
129
 
127
130
  def update_admin(self, account: AdminAccount) -> None:
128
- index = self._read_index(self.admin_dir)
129
- if account.email not in index:
130
- raise ValueError(f"Админ {account.email} не найден")
131
- info = index[account.email]
132
- if account.last_login:
133
- info["last_login"] = account.last_login
134
- self._write_index(self.admin_dir, index)
135
- account_dir = self.admin_dir / info["id"]
136
- meta: dict[str, Any] = {
137
- "email": account.email,
138
- "password": account.password,
139
- }
140
- if account.access_token is not None:
141
- meta["access_token"] = account.access_token
142
- if account.workspace_id is not None:
143
- meta["workspace_id"] = account.workspace_id
144
- if account.account_id is not None:
145
- meta["account_id"] = account.account_id
146
- if account.workspaces is not None:
147
- meta["workspaces"] = account.workspaces
148
- self._write_meta(account_dir, meta)
131
+ with self._lock:
132
+ index = self._read_index(self.admin_dir)
133
+ if account.email not in index:
134
+ raise ValueError(f"Админ {account.email} не найден")
135
+ info = index[account.email]
136
+ if account.last_login:
137
+ info["last_login"] = account.last_login
138
+ self._write_index(self.admin_dir, index)
139
+ account_dir = self.admin_dir / info["id"]
140
+ meta: dict[str, Any] = {
141
+ "email": account.email,
142
+ "password": account.password,
143
+ }
144
+ if account.access_token is not None:
145
+ meta["access_token"] = account.access_token
146
+ if account.workspace_id is not None:
147
+ meta["workspace_id"] = account.workspace_id
148
+ if account.account_id is not None:
149
+ meta["account_id"] = account.account_id
150
+ if account.workspaces is not None:
151
+ meta["workspaces"] = account.workspaces
152
+ self._write_meta(account_dir, meta)
149
153
 
150
154
  def delete_admin(self, email: str) -> None:
151
- index = self._read_index(self.admin_dir)
152
- info = index.pop(email, None)
153
- if not info:
154
- return
155
- self._write_index(self.admin_dir, index)
156
- account_dir = self.admin_dir / info["id"]
157
- if account_dir.exists():
158
- shutil.rmtree(account_dir)
155
+ with self._lock:
156
+ index = self._read_index(self.admin_dir)
157
+ info = index.pop(email, None)
158
+ if not info:
159
+ return
160
+ self._write_index(self.admin_dir, index)
161
+ account_dir = self.admin_dir / info["id"]
162
+ if account_dir.exists():
163
+ shutil.rmtree(account_dir)
159
164
 
160
165
  def get_admin_profile_dir(self, account: AdminAccount) -> Path:
161
166
  profile_dir = self.admin_dir / account.id / "browser_profile"
@@ -203,63 +208,66 @@ class AccountStore:
203
208
  )
204
209
 
205
210
  def add_worker(self, email: str, password: str, admin_email: str) -> WorkerAccount:
206
- index = self._read_index(self.worker_dir)
207
- if email in index:
208
- raise ValueError(f"Worker {email} уже существует")
209
- worker_id = uuid.uuid4().hex
210
- now = datetime.now(timezone.utc).isoformat()
211
- index[email] = {
212
- "id": worker_id,
213
- "status": "created",
214
- "admin_email": admin_email,
215
- "created_at": now,
216
- }
217
- self._write_index(self.worker_dir, index)
218
- account_dir = self.worker_dir / worker_id
219
- self._write_meta(account_dir, {
220
- "email": email,
221
- "password": password,
222
- "status": "created",
223
- })
224
- return WorkerAccount(
225
- id=worker_id, email=email, password=password,
226
- admin_email=admin_email, created_at=now,
227
- )
211
+ with self._lock:
212
+ index = self._read_index(self.worker_dir)
213
+ if email in index:
214
+ raise ValueError(f"Worker {email} уже существует")
215
+ worker_id = uuid.uuid4().hex
216
+ now = datetime.now(timezone.utc).isoformat()
217
+ index[email] = {
218
+ "id": worker_id,
219
+ "status": "created",
220
+ "admin_email": admin_email,
221
+ "created_at": now,
222
+ }
223
+ self._write_index(self.worker_dir, index)
224
+ account_dir = self.worker_dir / worker_id
225
+ self._write_meta(account_dir, {
226
+ "email": email,
227
+ "password": password,
228
+ "status": "created",
229
+ })
230
+ return WorkerAccount(
231
+ id=worker_id, email=email, password=password,
232
+ admin_email=admin_email, created_at=now,
233
+ )
228
234
 
229
235
  def update_worker(self, account: WorkerAccount) -> None:
230
- index = self._read_index(self.worker_dir)
231
- if account.email not in index:
232
- raise ValueError(f"Worker {account.email} не найден")
233
- info = index[account.email]
234
- info["status"] = account.status
235
- if account.admin_email is not None:
236
- info["admin_email"] = account.admin_email
237
- else:
238
- info.pop("admin_email", None)
239
- self._write_index(self.worker_dir, index)
240
- account_dir = self.worker_dir / info["id"]
241
- meta: dict[str, Any] = {
242
- "email": account.email,
243
- "password": account.password,
244
- "status": account.status,
245
- }
246
- if account.openai_password is not None:
247
- meta["openai_password"] = account.openai_password
248
- if account.access_token is not None:
249
- meta["access_token"] = account.access_token
250
- if account.workspace_id is not None:
251
- meta["workspace_id"] = account.workspace_id
252
- self._write_meta(account_dir, meta)
236
+ with self._lock:
237
+ index = self._read_index(self.worker_dir)
238
+ if account.email not in index:
239
+ raise ValueError(f"Worker {account.email} не найден")
240
+ info = index[account.email]
241
+ info["status"] = account.status
242
+ if account.admin_email is not None:
243
+ info["admin_email"] = account.admin_email
244
+ else:
245
+ info.pop("admin_email", None)
246
+ self._write_index(self.worker_dir, index)
247
+ account_dir = self.worker_dir / info["id"]
248
+ meta: dict[str, Any] = {
249
+ "email": account.email,
250
+ "password": account.password,
251
+ "status": account.status,
252
+ }
253
+ if account.openai_password is not None:
254
+ meta["openai_password"] = account.openai_password
255
+ if account.access_token is not None:
256
+ meta["access_token"] = account.access_token
257
+ if account.workspace_id is not None:
258
+ meta["workspace_id"] = account.workspace_id
259
+ self._write_meta(account_dir, meta)
253
260
 
254
261
  def delete_worker(self, email: str) -> None:
255
- index = self._read_index(self.worker_dir)
256
- info = index.pop(email, None)
257
- if not info:
258
- return
259
- self._write_index(self.worker_dir, index)
260
- account_dir = self.worker_dir / info["id"]
261
- if account_dir.exists():
262
- shutil.rmtree(account_dir)
262
+ with self._lock:
263
+ index = self._read_index(self.worker_dir)
264
+ info = index.pop(email, None)
265
+ if not info:
266
+ return
267
+ self._write_index(self.worker_dir, index)
268
+ account_dir = self.worker_dir / info["id"]
269
+ if account_dir.exists():
270
+ shutil.rmtree(account_dir)
263
271
 
264
272
  def get_worker_profile_dir(self, account: WorkerAccount) -> Path:
265
273
  profile_dir = self.worker_dir / account.id / "browser_profile"
package/backend/jobs.py CHANGED
@@ -67,7 +67,6 @@ class JobManager:
67
67
  {
68
68
  "job_id": job_id,
69
69
  "error": message,
70
- "traceback": tb,
71
70
  "log_path": job_logger.rel_path,
72
71
  },
73
72
  )
@@ -133,7 +133,7 @@ class BoomlifyProvider(MailProvider):
133
133
  if not email or not email_id:
134
134
  raise MailError(f"Unexpected Boomlify create response: {raw}")
135
135
 
136
- return Mailbox(email=email, password=f"boomlify:{email_id}")
136
+ return Mailbox(email=email, password=f"{self.password_prefix}{email_id}")
137
137
 
138
138
  def inbox(self, mailbox: Mailbox) -> Inbox:
139
139
  email_id = self._extract_mailbox_id(mailbox)
@@ -72,7 +72,7 @@ class RPCServer:
72
72
  masked = ""
73
73
  if value:
74
74
  masked = value[:4] + "***" + value[-4:] if len(value) > 12 else "***"
75
- items.append({"key": key, "label": label, "value": value, "masked": masked})
75
+ items.append({"key": key, "label": label, "masked": masked})
76
76
  return {"items": items, "path": str(self._settings_path())}
77
77
 
78
78
  def _set_setting(self, key: str, value: str) -> None:
@@ -228,7 +228,9 @@ class RPCServer:
228
228
 
229
229
  if m == "settings.set":
230
230
  key = self._as_str_param(p, "key")
231
- value = self._as_str_param(p, "value")
231
+ value = p.get("value")
232
+ if not isinstance(value, str):
233
+ raise RPCError(-32602, "Invalid params", {"details": "'value' must be string"})
232
234
  self._set_setting(key, value)
233
235
  return make_success_response(req.request_id, {"ok": True})
234
236
 
@@ -268,10 +270,7 @@ class RPCServer:
268
270
  RPCError(
269
271
  -32000,
270
272
  "Server error",
271
- {
272
- "details": str(e),
273
- "traceback": tb,
274
- },
273
+ {"details": str(e)},
275
274
  ),
276
275
  )
277
276
 
@@ -221,14 +221,24 @@ class UIFacade:
221
221
  log(f"Браузер {label} открыт. Закройте окно браузера для возврата.")
222
222
  wait_for_browser_close(context, log=log)
223
223
 
224
+ def _replace_manager(self, manager: SlotManager) -> None:
225
+ if self.manager:
226
+ try:
227
+ self.manager.close()
228
+ except Exception:
229
+ pass
230
+ self.manager = manager
231
+
224
232
  def login_admin(self, email: str, log: LogFunc) -> None:
225
- self.manager = SlotManager(store=self.store, admin_email=email, log=log)
226
- self.manager.login_admin()
233
+ manager = SlotManager(store=self.store, admin_email=email, log=log)
234
+ self._replace_manager(manager)
235
+ manager.login_admin()
227
236
  self.sync_codex_files()
228
237
 
229
238
  def login_admin_manual(self, email: str, log: LogFunc) -> None:
230
- self.manager = SlotManager(store=self.store, admin_email=email, log=log)
231
- self.manager.login_admin_manual()
239
+ manager = SlotManager(store=self.store, admin_email=email, log=log)
240
+ self._replace_manager(manager)
241
+ manager.login_admin_manual()
232
242
  self.sync_codex_files()
233
243
 
234
244
  def run_slots_pipeline(
@@ -238,7 +248,8 @@ class UIFacade:
238
248
  log: LogFunc,
239
249
  progress: ProgressFunc | None = None,
240
250
  ) -> dict[str, int]:
241
- self.manager = SlotManager(store=self.store, admin_email=admin_email, log=log, headless=False)
251
+ manager = SlotManager(store=self.store, admin_email=admin_email, log=log, headless=False)
252
+ self._replace_manager(manager)
242
253
  ok = 0
243
254
  for i in range(count):
244
255
  slot_no = i + 1
@@ -246,11 +257,11 @@ class UIFacade:
246
257
  if progress:
247
258
  progress(slot_no, count, f"slot {slot_no}/{count}")
248
259
  try:
249
- self.manager.create_invite_login_one()
260
+ manager.create_invite_login_one()
250
261
  ok += 1
251
262
  except Exception as e:
252
263
  log(f"Ошибка: {e}")
253
- self.manager._close_admin_page()
264
+ manager._close_admin_page()
254
265
  log(f"Готово: {ok}/{count} слотов")
255
266
  self.sync_codex_files()
256
267
  return {"ok": ok, "total": count}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "izteamslots",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "ChatGPT Team slot management — automated invite, register, OAuth login & codex token pipeline",
5
5
  "bin": {
6
6
  "izteamslots": "bin/izteamslots.mjs"
@@ -50,7 +50,6 @@ export interface MailAccountRow {
50
50
  export interface SettingItem {
51
51
  key: string
52
52
  label: string
53
- value: string
54
53
  masked: string
55
54
  }
56
55
 
@@ -375,7 +375,7 @@ export class MainScreen {
375
375
 
376
376
  private async loadSettings() {
377
377
  try {
378
- const result = await this.rpc.request<{ items: Array<{ key: string; label: string; value: string; masked: string }> }>("settings.get")
378
+ const result = await this.rpc.request<{ items: Array<{ key: string; label: string; masked: string }> }>("settings.get")
379
379
  this.state = { ...this.state, settings: result.items }
380
380
  } catch (error) {
381
381
  this.pushLog(`Ошибка загрузки настроек: ${String(error)}`)