@uniquedivine/tg-sai 0.0.4 → 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 +41 -0
- package/RUNBOOK.md +204 -24
- package/actionExecutor.ts +183 -131
- package/adminAudit.ts +60 -60
- package/captureProvisioningGolden.ts +98 -0
- package/cfg.ts +12 -1
- package/custodyWorker.ts +501 -0
- package/custodyWorkerCli.ts +58 -0
- package/health.ts +31 -50
- package/main.ts +860 -214
- package/mtcuteAdapter.ts +266 -10
- package/ops.ts +12 -0
- package/package.json +14 -5
- package/profiles.ts +157 -32
- package/provisioning.ts +805 -0
- package/provisioningCli.ts +147 -0
- package/provisioningCloud.ts +214 -0
- package/provisioningGoldens.ts +70 -0
- package/provisioningRuntime.ts +417 -0
- package/runtimeAssets.d.ts +4 -0
- package/runtimeHealth.ts +132 -0
- package/runtimeHealthCli.ts +48 -0
- package/session_string_generator.py +36 -24
- package/state/cli.ts +641 -0
- package/state/db.ts +72 -0
- package/state/guard.ts +270 -0
- package/state/holds.ts +381 -0
- package/state/migrations/0001-flood-hold.sql +16 -0
- package/telegramRuntime.ts +25 -0
- package/telemetry/cli.ts +698 -0
- package/telemetry/db.ts +471 -0
- package/telemetry/middleware.ts +475 -0
- package/telemetry/migrations/0001-event.sql +33 -0
- package/telemetry/migrations/0002-event-job-index.sql +2 -0
- package/telemetry/migrations/0003-event-log.sql +10 -0
- package/telemetry/queries.ts +475 -0
- package/telemetry/run.ts +491 -0
- package/telemetry/sqlAssets.d.ts +4 -0
- package/telemetry/summarize.ts +219 -0
package/profiles.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
2
|
import { homedir, platform as osPlatform } from "node:os"
|
|
3
3
|
import { dirname, join } from "node:path"
|
|
4
|
-
|
|
5
4
|
import type { UserData } from "./adminAudit"
|
|
6
5
|
|
|
7
6
|
const TG_SAI_DIR_ENV = "TG_SAI_DIR"
|
|
@@ -44,12 +43,16 @@ export interface TgSaiProfile {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
export interface TgSaiConfig {
|
|
46
|
+
profiles: TgSaiProfile[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LegacyTgSaiConfig {
|
|
47
50
|
activeProfile: string | null
|
|
48
51
|
profiles: Record<string, TgSaiProfile>
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export interface ProfileAddInput {
|
|
52
|
-
name
|
|
55
|
+
name?: string
|
|
53
56
|
apiId: number | null
|
|
54
57
|
apiHash: string
|
|
55
58
|
sessionString: string
|
|
@@ -139,8 +142,7 @@ export const WINDOWS_DISCOVERY_PATHS = [
|
|
|
139
142
|
] as const
|
|
140
143
|
|
|
141
144
|
export const emptyConfig = (): TgSaiConfig => ({
|
|
142
|
-
|
|
143
|
-
profiles: {},
|
|
145
|
+
profiles: [],
|
|
144
146
|
})
|
|
145
147
|
|
|
146
148
|
export function homeDir(env: ProfileStoreEnv = process.env): string {
|
|
@@ -236,11 +238,12 @@ export async function readConfig(
|
|
|
236
238
|
): Promise<TgSaiConfig> {
|
|
237
239
|
try {
|
|
238
240
|
const raw = await readFile(configPath(env), "utf8")
|
|
239
|
-
const parsed = JSON.parse(raw) as TgSaiConfig
|
|
240
|
-
|
|
241
|
+
const parsed = JSON.parse(raw) as TgSaiConfig | LegacyTgSaiConfig
|
|
242
|
+
const config = normalizeConfig(parsed)
|
|
243
|
+
for (const profile of config.profiles) {
|
|
241
244
|
profile.password = profile.password ?? ""
|
|
242
245
|
}
|
|
243
|
-
return
|
|
246
|
+
return config
|
|
244
247
|
} catch (error) {
|
|
245
248
|
if (
|
|
246
249
|
error instanceof Error &&
|
|
@@ -253,6 +256,44 @@ export async function readConfig(
|
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
export function normalizeConfig(
|
|
260
|
+
config: TgSaiConfig | LegacyTgSaiConfig,
|
|
261
|
+
): TgSaiConfig {
|
|
262
|
+
if (Array.isArray(config.profiles)) {
|
|
263
|
+
return {
|
|
264
|
+
profiles: config.profiles.map((profile, index) =>
|
|
265
|
+
normalizeProfile(profile, index),
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const legacyConfig = config as LegacyTgSaiConfig
|
|
271
|
+
const legacyProfiles = Object.entries(legacyConfig.profiles)
|
|
272
|
+
const orderedNames = [
|
|
273
|
+
...(legacyConfig.activeProfile &&
|
|
274
|
+
legacyConfig.profiles[legacyConfig.activeProfile]
|
|
275
|
+
? [legacyConfig.activeProfile]
|
|
276
|
+
: []),
|
|
277
|
+
...legacyProfiles
|
|
278
|
+
.map(([name]) => name)
|
|
279
|
+
.filter((name) => name !== legacyConfig.activeProfile),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
profiles: orderedNames.map((name, index) =>
|
|
284
|
+
normalizeProfile({ ...legacyConfig.profiles[name]!, name }, index),
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeProfile(profile: TgSaiProfile, index: number): TgSaiProfile {
|
|
290
|
+
return {
|
|
291
|
+
...profile,
|
|
292
|
+
name: profile.name || profile.handle || `profile-${index}`,
|
|
293
|
+
password: profile.password ?? "",
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
256
297
|
export async function writeConfig(
|
|
257
298
|
config: TgSaiConfig,
|
|
258
299
|
env: ProfileStoreEnv = process.env,
|
|
@@ -273,25 +314,46 @@ export async function clearConfig(
|
|
|
273
314
|
export function profileFromInput(
|
|
274
315
|
input: ProfileAddInput,
|
|
275
316
|
now = new Date(),
|
|
317
|
+
index = 0,
|
|
276
318
|
): TgSaiProfile {
|
|
277
319
|
const timestamp = now.toISOString()
|
|
278
320
|
return {
|
|
279
321
|
...input,
|
|
322
|
+
name: profileNameFromInput(input, index),
|
|
280
323
|
createdAt: timestamp,
|
|
281
324
|
updatedAt: timestamp,
|
|
282
325
|
}
|
|
283
326
|
}
|
|
284
327
|
|
|
328
|
+
function profileNameFromInput(input: ProfileAddInput, index: number): string {
|
|
329
|
+
return (
|
|
330
|
+
input.name ??
|
|
331
|
+
input.handle ??
|
|
332
|
+
(input.userId !== null ? String(input.userId) : null) ??
|
|
333
|
+
`profile-${index}`
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
285
337
|
export async function addProfile(
|
|
286
338
|
input: ProfileAddInput,
|
|
287
339
|
env: ProfileStoreEnv = process.env,
|
|
288
340
|
now = new Date(),
|
|
289
341
|
): Promise<TgSaiConfig> {
|
|
290
342
|
const config = await readConfig(env)
|
|
291
|
-
const
|
|
343
|
+
const index =
|
|
344
|
+
input.name !== undefined
|
|
345
|
+
? findProfileIndex(config, input.name)
|
|
346
|
+
: config.profiles.length > 0
|
|
347
|
+
? 0
|
|
348
|
+
: -1
|
|
349
|
+
const targetIndex = index >= 0 ? index : config.profiles.length
|
|
350
|
+
const previous = index >= 0 ? config.profiles[index] : undefined
|
|
351
|
+
if (previous?.userId != null && input.userId != null && input.userId !== previous.userId) {
|
|
352
|
+
throw new Error("Cannot replace a saved profile's account identity. Use a new profile name or explicitly remove the old profile first.")
|
|
353
|
+
}
|
|
292
354
|
const timestamp = now.toISOString()
|
|
293
|
-
|
|
294
|
-
name: input
|
|
355
|
+
const nextProfile: TgSaiProfile = {
|
|
356
|
+
name: profileNameFromInput(input, targetIndex),
|
|
295
357
|
apiId: input.apiId ?? previous?.apiId ?? null,
|
|
296
358
|
apiHash: input.apiHash || previous?.apiHash || "",
|
|
297
359
|
sessionString: input.sessionString || previous?.sessionString || "",
|
|
@@ -303,9 +365,10 @@ export async function addProfile(
|
|
|
303
365
|
updatedAt: timestamp,
|
|
304
366
|
}
|
|
305
367
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
368
|
+
if (index >= 0) {
|
|
369
|
+
config.profiles[index] = nextProfile
|
|
370
|
+
} else {
|
|
371
|
+
config.profiles.push(nextProfile)
|
|
309
372
|
}
|
|
310
373
|
|
|
311
374
|
await writeConfig(config, env)
|
|
@@ -313,40 +376,69 @@ export async function addProfile(
|
|
|
313
376
|
}
|
|
314
377
|
|
|
315
378
|
export async function removeProfile(
|
|
316
|
-
|
|
379
|
+
selector: string,
|
|
317
380
|
env: ProfileStoreEnv = process.env,
|
|
318
381
|
): Promise<TgSaiConfig> {
|
|
319
382
|
const config = await readConfig(env)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const remaining = Object.keys(config.profiles)
|
|
324
|
-
config.activeProfile = remaining.length === 1 ? remaining[0]! : null
|
|
383
|
+
const index = findProfileIndex(config, selector)
|
|
384
|
+
if (index === -1) {
|
|
385
|
+
throw new Error(`Unknown profile: ${selector}`)
|
|
325
386
|
}
|
|
387
|
+
config.profiles.splice(index, 1)
|
|
326
388
|
|
|
327
389
|
await writeConfig(config, env)
|
|
328
390
|
return config
|
|
329
391
|
}
|
|
330
392
|
|
|
331
393
|
export async function setActiveProfile(
|
|
332
|
-
|
|
394
|
+
selector: string,
|
|
333
395
|
env: ProfileStoreEnv = process.env,
|
|
334
396
|
): Promise<TgSaiConfig> {
|
|
335
397
|
const config = await readConfig(env)
|
|
336
|
-
|
|
337
|
-
|
|
398
|
+
const index = findProfileIndex(config, selector)
|
|
399
|
+
if (index === -1) {
|
|
400
|
+
throw new Error(`Unknown profile: ${selector}`)
|
|
338
401
|
}
|
|
339
|
-
config.
|
|
402
|
+
const [profile] = config.profiles.splice(index, 1)
|
|
403
|
+
config.profiles.unshift(profile!)
|
|
340
404
|
await writeConfig(config, env)
|
|
341
405
|
return config
|
|
342
406
|
}
|
|
343
407
|
|
|
344
408
|
export function getProfile(
|
|
345
409
|
config: TgSaiConfig,
|
|
346
|
-
|
|
410
|
+
selector: string | undefined,
|
|
347
411
|
): TgSaiProfile | null {
|
|
348
|
-
|
|
349
|
-
|
|
412
|
+
if (selector === undefined) {
|
|
413
|
+
return config.profiles[0] ?? null
|
|
414
|
+
}
|
|
415
|
+
const index = findProfileIndex(config, selector)
|
|
416
|
+
return index === -1 ? null : config.profiles[index]!
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function findProfileIndex(
|
|
420
|
+
config: TgSaiConfig,
|
|
421
|
+
selector: string,
|
|
422
|
+
): number {
|
|
423
|
+
const exactMatch = config.profiles.findIndex(
|
|
424
|
+
(profile) =>
|
|
425
|
+
profile.name === selector ||
|
|
426
|
+
profile.handle === selector ||
|
|
427
|
+
String(profile.userId) === selector,
|
|
428
|
+
)
|
|
429
|
+
if (exactMatch !== -1) {
|
|
430
|
+
return exactMatch
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const asIndex = Number(selector)
|
|
434
|
+
if (Number.isInteger(asIndex) && asIndex >= 0) {
|
|
435
|
+
return asIndex < config.profiles.length ? asIndex : -1
|
|
436
|
+
}
|
|
437
|
+
return -1
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function profileLabel(profile: TgSaiProfile, index: number): string {
|
|
441
|
+
return profile.handle ?? profile.name ?? `profile-${index}`
|
|
350
442
|
}
|
|
351
443
|
|
|
352
444
|
export function redactedProfile(profile: TgSaiProfile) {
|
|
@@ -679,13 +771,24 @@ export async function credentialsFromProfile(
|
|
|
679
771
|
return credentialsFromProfileRecord(profile)
|
|
680
772
|
}
|
|
681
773
|
|
|
682
|
-
|
|
774
|
+
/** Where `resolveTelegramCredentialSource` found the credentials. */
|
|
775
|
+
export type TelegramCredentialSource = "env" | "profile" | "known_file"
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Credentials for `profileName` and where they came from: `TELEGRAM_*`
|
|
779
|
+
* environment variables, the selected profile, or a known credential file.
|
|
780
|
+
* Only `profile` credentials match the profile's saved `userId`.
|
|
781
|
+
*/
|
|
782
|
+
export async function resolveTelegramCredentialSource(
|
|
683
783
|
profileName: string | undefined,
|
|
684
784
|
env: ProfileStoreEnv = process.env,
|
|
685
|
-
): Promise<
|
|
785
|
+
): Promise<{
|
|
786
|
+
credentials: TelegramCredentials
|
|
787
|
+
source: TelegramCredentialSource
|
|
788
|
+
}> {
|
|
686
789
|
const envCredentials = credentialsFromEnv(env)
|
|
687
790
|
if (envCredentials) {
|
|
688
|
-
return envCredentials
|
|
791
|
+
return { credentials: envCredentials, source: "env" }
|
|
689
792
|
}
|
|
690
793
|
|
|
691
794
|
const config = await readConfig(env)
|
|
@@ -694,7 +797,7 @@ export async function resolveTelegramCredentials(
|
|
|
694
797
|
? credentialsFromProfileRecord(selectedProfile)
|
|
695
798
|
: null
|
|
696
799
|
if (profileCredentials) {
|
|
697
|
-
return profileCredentials
|
|
800
|
+
return { credentials: profileCredentials, source: "profile" }
|
|
698
801
|
}
|
|
699
802
|
if (selectedProfile) {
|
|
700
803
|
throw new Error(
|
|
@@ -704,7 +807,7 @@ export async function resolveTelegramCredentials(
|
|
|
704
807
|
|
|
705
808
|
const knownFileCredentials = await credentialsFromKnownFiles(env)
|
|
706
809
|
if (knownFileCredentials) {
|
|
707
|
-
return knownFileCredentials
|
|
810
|
+
return { credentials: knownFileCredentials, source: "known_file" }
|
|
708
811
|
}
|
|
709
812
|
|
|
710
813
|
throw new Error(
|
|
@@ -712,8 +815,15 @@ export async function resolveTelegramCredentials(
|
|
|
712
815
|
)
|
|
713
816
|
}
|
|
714
817
|
|
|
818
|
+
export async function resolveTelegramCredentials(
|
|
819
|
+
profileName: string | undefined,
|
|
820
|
+
env: ProfileStoreEnv = process.env,
|
|
821
|
+
): Promise<TelegramCredentials> {
|
|
822
|
+
return (await resolveTelegramCredentialSource(profileName, env)).credentials
|
|
823
|
+
}
|
|
824
|
+
|
|
715
825
|
export function profileInputFromUser(
|
|
716
|
-
name: string,
|
|
826
|
+
name: string | undefined,
|
|
717
827
|
credentials: TelegramCredentials,
|
|
718
828
|
user: UserData,
|
|
719
829
|
password = "",
|
|
@@ -729,3 +839,18 @@ export function profileInputFromUser(
|
|
|
729
839
|
displayName: user.displayName,
|
|
730
840
|
}
|
|
731
841
|
}
|
|
842
|
+
|
|
843
|
+
/** Provisioning must never inherit another account from environment or discovery. */
|
|
844
|
+
export async function strictProfileCredentials(
|
|
845
|
+
selector: string,
|
|
846
|
+
expectedUserId: number,
|
|
847
|
+
env: ProfileStoreEnv = process.env,
|
|
848
|
+
): Promise<TelegramCredentials> {
|
|
849
|
+
if (!selector?.trim()) throw new Error("Explicit custody profile required")
|
|
850
|
+
const profile = getProfile(await readConfig(env), selector)
|
|
851
|
+
if (!profile) throw new Error("Custody profile is not configured")
|
|
852
|
+
if (profile.userId !== expectedUserId) throw new Error("Custody profile user ID mismatch")
|
|
853
|
+
const credentials = credentialsFromProfileRecord(profile)
|
|
854
|
+
if (!credentials) throw new Error("Custody profile is incomplete")
|
|
855
|
+
return credentials
|
|
856
|
+
}
|