agent-messenger 2.28.0 → 2.29.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +6 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +10 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +24 -2
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +343 -53
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +123 -11
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +17 -1
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/types.d.ts +10 -8
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/teams/app-config.d.ts +36 -0
- package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
- package/dist/src/platforms/teams/app-config.js +69 -0
- package/dist/src/platforms/teams/app-config.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +20 -2
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +311 -7
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +137 -3
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts +3 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +59 -2
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts +11 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +121 -2
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +46 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/device-code.d.ts +48 -0
- package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-code.js +199 -0
- package/dist/src/platforms/teams/device-code.js.map +1 -0
- package/dist/src/platforms/teams/device-login.d.ts +37 -0
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-login.js +129 -0
- package/dist/src/platforms/teams/device-login.js.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +12 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/index.d.ts +7 -3
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js +5 -2
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts +40 -0
- package/dist/src/platforms/teams/listener.d.ts.map +1 -0
- package/dist/src/platforms/teams/listener.js +286 -0
- package/dist/src/platforms/teams/listener.js.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.js +33 -0
- package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +60 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts +25 -0
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-provider.js +190 -0
- package/dist/src/platforms/teams/token-provider.js.map +1 -0
- package/dist/src/platforms/teams/trouter.d.ts +38 -0
- package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
- package/dist/src/platforms/teams/trouter.js +266 -0
- package/dist/src/platforms/teams/trouter.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +103 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +30 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +1 -1
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.js +16 -0
- package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.js +21 -0
- package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
- package/dist/src/tui/app.js +3 -3
- package/docs/content/docs/cli/instagram.mdx +1 -0
- package/docs/content/docs/cli/teams.mdx +36 -0
- package/docs/content/docs/sdk/teams.mdx +30 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-instagram/references/authentication.md +11 -0
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +82 -6
- package/skills/agent-teams/references/authentication.md +31 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +86 -0
- package/src/platforms/discord/client.ts +10 -3
- package/src/platforms/instagram/client.test.ts +478 -33
- package/src/platforms/instagram/client.ts +449 -68
- package/src/platforms/instagram/commands/auth.test.ts +177 -1
- package/src/platforms/instagram/commands/auth.ts +182 -11
- package/src/platforms/instagram/credential-manager.test.ts +30 -0
- package/src/platforms/instagram/credential-manager.ts +23 -1
- package/src/platforms/instagram/types.ts +11 -8
- package/src/platforms/teams/app-config.ts +92 -0
- package/src/platforms/teams/client.test.ts +335 -1
- package/src/platforms/teams/client.ts +370 -7
- package/src/platforms/teams/commands/auth.test.ts +93 -1
- package/src/platforms/teams/commands/auth.ts +182 -3
- package/src/platforms/teams/commands/file.test.ts +92 -1
- package/src/platforms/teams/commands/file.ts +76 -1
- package/src/platforms/teams/commands/message.test.ts +95 -1
- package/src/platforms/teams/commands/message.ts +145 -3
- package/src/platforms/teams/credential-manager.test.ts +20 -0
- package/src/platforms/teams/credential-manager.ts +68 -3
- package/src/platforms/teams/device-code.test.ts +311 -0
- package/src/platforms/teams/device-code.ts +277 -0
- package/src/platforms/teams/device-login.test.ts +288 -0
- package/src/platforms/teams/device-login.ts +206 -0
- package/src/platforms/teams/ensure-auth.ts +11 -0
- package/src/platforms/teams/index.test.ts +10 -0
- package/src/platforms/teams/index.ts +17 -1
- package/src/platforms/teams/listener.test.ts +243 -0
- package/src/platforms/teams/listener.ts +351 -0
- package/src/platforms/teams/realm-discovery.test.ts +73 -0
- package/src/platforms/teams/realm-discovery.ts +42 -0
- package/src/platforms/teams/token-extractor.ts +69 -0
- package/src/platforms/teams/token-provider.test.ts +227 -0
- package/src/platforms/teams/token-provider.ts +284 -0
- package/src/platforms/teams/trouter.test.ts +174 -0
- package/src/platforms/teams/trouter.ts +323 -0
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +96 -0
- package/src/tui/adapters/instagram-adapter.ts +13 -0
- package/src/tui/adapters/teams-adapter.ts +23 -0
- package/src/tui/app.ts +3 -3
|
@@ -9,24 +9,101 @@ import {
|
|
|
9
9
|
extractMessageText,
|
|
10
10
|
getMessageType,
|
|
11
11
|
type InstagramChatSummary,
|
|
12
|
+
type InstagramDevice,
|
|
12
13
|
type InstagramMessageSummary,
|
|
13
14
|
type InstagramSessionState,
|
|
14
15
|
} from './types'
|
|
15
16
|
|
|
16
17
|
const IG_BASE_URL = 'https://i.instagram.com/api/v1'
|
|
17
18
|
const IG_APP_ID = '567067343352427'
|
|
18
|
-
const IG_VERSION = '
|
|
19
|
-
const IG_VERSION_CODE = '
|
|
20
|
-
|
|
21
|
-
const
|
|
19
|
+
const IG_VERSION = '428.0.0.47.67'
|
|
20
|
+
const IG_VERSION_CODE = '719358530'
|
|
21
|
+
// Bloks client versioning id; required header for Bloks endpoints (2FA / CAA flows).
|
|
22
|
+
const IG_BLOKS_VERSION_ID = '5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73'
|
|
23
|
+
const IG_CAPABILITIES = '3brTv10='
|
|
24
|
+
const IG_LOCALE = 'en_US'
|
|
25
|
+
// Instagram silently holds the socket open (no response) for flagged IPs / fingerprints instead
|
|
26
|
+
// of returning an error, so an unbounded fetch hangs forever. Cap every request and surface a
|
|
27
|
+
// clear, actionable error instead of a frozen process.
|
|
28
|
+
const IG_REQUEST_TIMEOUT_MS = 30_000
|
|
29
|
+
const ANDROID_VERSION = '14'
|
|
30
|
+
const ANDROID_RELEASE = '34'
|
|
22
31
|
const DEVICE_DPI = '480dpi'
|
|
23
|
-
const DEVICE_RESOLUTION = '
|
|
24
|
-
const DEVICE_MANUFACTURER = '
|
|
25
|
-
const DEVICE_MODEL = '
|
|
26
|
-
const
|
|
32
|
+
const DEVICE_RESOLUTION = '1344x2992'
|
|
33
|
+
const DEVICE_MANUFACTURER = 'Google/google'
|
|
34
|
+
const DEVICE_MODEL = 'Pixel 8 Pro'
|
|
35
|
+
const DEVICE_NAME = 'husky'
|
|
36
|
+
const DEVICE_CHIPSET = 'husky'
|
|
27
37
|
|
|
28
38
|
export function generateDeviceString(): string {
|
|
29
|
-
return `${
|
|
39
|
+
return `${ANDROID_RELEASE}/${ANDROID_VERSION}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_NAME}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Instagram no longer verifies the body HMAC; the current mobile API expects a literal
|
|
43
|
+
// "SIGNATURE." prefix (per subzeroid/instagrapi, subzeroid/aiograpi). Sending a real HMAC +
|
|
44
|
+
// ig_sig_key_version is rejected as malformed on /accounts/login/, surfacing as a misleading
|
|
45
|
+
// "account not found" error. URLSearchParams url-encodes the value exactly once.
|
|
46
|
+
function signBody(payload: Record<string, unknown>): Record<string, string> {
|
|
47
|
+
return { signed_body: `SIGNATURE.${JSON.stringify(payload)}` }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// jazoest = "2" + sum of ASCII byte values of the input; an anti-bot checksum expected by login.
|
|
51
|
+
function createJazoest(input: string): string {
|
|
52
|
+
let sum = 0
|
|
53
|
+
for (let i = 0; i < input.length; i++) {
|
|
54
|
+
sum += input.charCodeAt(i)
|
|
55
|
+
}
|
|
56
|
+
return `2${sum}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function generateToken(): string {
|
|
60
|
+
return randomBytes(16).toString('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isTimeoutAbort(error: unknown): boolean {
|
|
64
|
+
// AbortSignal.timeout aborts the connect AND the body stream, surfacing as TimeoutError on the
|
|
65
|
+
// fetch and AbortError when a stalled body read is aborted mid-stream; both mean "IG stalled".
|
|
66
|
+
return error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stalledError(path: string): InstagramError {
|
|
70
|
+
return new InstagramError(
|
|
71
|
+
`Instagram did not respond within ${IG_REQUEST_TIMEOUT_MS / 1000}s (${path}). The connection stalled, ` +
|
|
72
|
+
'which usually means the IP is flagged (VPN/datacenter/proxy or prior automated attempts) or the ' +
|
|
73
|
+
'device fingerprint was rejected. Try "agent-instagram auth extract" from a browser logged into ' +
|
|
74
|
+
'instagram.com instead of password login.',
|
|
75
|
+
'request_stalled',
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TimedRequest {
|
|
80
|
+
response: Response
|
|
81
|
+
readText: () => Promise<string>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// One AbortSignal.timeout covers the whole request lifecycle: connect, headers, AND the body read.
|
|
85
|
+
// Reading the body through readText() keeps a post-header stall mapped to request_stalled instead
|
|
86
|
+
// of leaking out as a generic parse failure.
|
|
87
|
+
async function fetchWithTimeout(url: string, init: RequestInit, path: string): Promise<TimedRequest> {
|
|
88
|
+
const signal = AbortSignal.timeout(IG_REQUEST_TIMEOUT_MS)
|
|
89
|
+
let response: Response
|
|
90
|
+
try {
|
|
91
|
+
response = await fetch(url, { ...init, signal })
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isTimeoutAbort(error)) throw stalledError(path)
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const readText = async (): Promise<string> => {
|
|
98
|
+
try {
|
|
99
|
+
return await response.text()
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (isTimeoutAbort(error)) throw stalledError(path)
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { response, readText }
|
|
30
107
|
}
|
|
31
108
|
|
|
32
109
|
function buildUserAgent(deviceString: string): string {
|
|
@@ -37,6 +114,29 @@ export function generateAndroidDeviceId(): string {
|
|
|
37
114
|
return `android-${randomBytes(8).toString('hex')}`
|
|
38
115
|
}
|
|
39
116
|
|
|
117
|
+
export function generateDevice(): InstagramDevice {
|
|
118
|
+
return {
|
|
119
|
+
phone_id: randomUUID(),
|
|
120
|
+
uuid: randomUUID(),
|
|
121
|
+
android_device_id: generateAndroidDeviceId(),
|
|
122
|
+
advertising_id: randomUUID(),
|
|
123
|
+
client_session_id: randomUUID(),
|
|
124
|
+
device_string: generateDeviceString(),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseOneClickLoginLink(input: string): { uid: string; token: string } | null {
|
|
129
|
+
const trimmed = input.trim()
|
|
130
|
+
let query = trimmed.includes('?') ? trimmed.slice(trimmed.indexOf('?') + 1) : trimmed
|
|
131
|
+
const hashIdx = query.indexOf('#')
|
|
132
|
+
if (hashIdx !== -1) query = query.slice(0, hashIdx)
|
|
133
|
+
const params = new URLSearchParams(query)
|
|
134
|
+
const uid = params.get('uid')
|
|
135
|
+
const token = params.get('token')
|
|
136
|
+
if (uid && token) return { uid, token }
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
// Instagram DM timestamps are in microseconds
|
|
41
141
|
function microsecondsToISO(us: number): string {
|
|
42
142
|
return new Date(us / 1000).toISOString()
|
|
@@ -48,14 +148,40 @@ export class InstagramClient {
|
|
|
48
148
|
private sessionPath: string | null = null
|
|
49
149
|
private userId: string | null = null
|
|
50
150
|
private cookies: Map<string, string> = new Map()
|
|
151
|
+
private countryCode = '1'
|
|
51
152
|
|
|
52
153
|
constructor(credentialManager?: InstagramCredentialManager) {
|
|
53
154
|
this.credentialManager = credentialManager ?? new InstagramCredentialManager()
|
|
54
155
|
}
|
|
55
156
|
|
|
157
|
+
setCountryCode(code: string): void {
|
|
158
|
+
this.countryCode = code
|
|
159
|
+
}
|
|
160
|
+
|
|
56
161
|
async login(credentials?: { username: string; password: string }, accountId?: string): Promise<this> {
|
|
57
162
|
if (credentials) {
|
|
58
|
-
await this.authenticate(credentials.username, credentials.password)
|
|
163
|
+
const result = await this.authenticate(credentials.username, credentials.password)
|
|
164
|
+
if (!result.userId) {
|
|
165
|
+
if (result.requiresTwoFactor) {
|
|
166
|
+
throw new InstagramError(
|
|
167
|
+
'Two-factor authentication required. Use the CLI (auth login/verify) to complete 2FA.',
|
|
168
|
+
'two_factor_required',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (result.challengeRequired) {
|
|
172
|
+
throw new InstagramError(
|
|
173
|
+
'Instagram requires a security challenge. Use the CLI (auth login/challenge) to resolve it.',
|
|
174
|
+
'challenge_required',
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
if (result.oneClickEmailAvailable) {
|
|
178
|
+
throw new InstagramError(
|
|
179
|
+
'Password login was rejected; this account can log in by email. Use the CLI "auth login-email" to complete it.',
|
|
180
|
+
'one_click_email_available',
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
throw new InstagramError('Login did not complete.', 'login_incomplete')
|
|
184
|
+
}
|
|
59
185
|
return this
|
|
60
186
|
}
|
|
61
187
|
|
|
@@ -100,42 +226,40 @@ export class InstagramClient {
|
|
|
100
226
|
twoFactorInfo?: Record<string, unknown>
|
|
101
227
|
challengeRequired?: boolean
|
|
102
228
|
challengePath?: string
|
|
229
|
+
oneClickEmailAvailable?: boolean
|
|
103
230
|
}> {
|
|
104
|
-
const
|
|
105
|
-
const device = {
|
|
106
|
-
phone_id: randomUUID(),
|
|
107
|
-
uuid: randomUUID(),
|
|
108
|
-
android_device_id: generateAndroidDeviceId(),
|
|
109
|
-
advertising_id: randomUUID(),
|
|
110
|
-
client_session_id: randomUUID(),
|
|
111
|
-
device_string: deviceString,
|
|
112
|
-
}
|
|
113
|
-
|
|
231
|
+
const device = await this.resolveDevice()
|
|
114
232
|
this.session = { cookies: '', device }
|
|
115
233
|
|
|
116
234
|
const encryptionKey = await this.preLoginFlow()
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
this.debugLog?.('no encryption key, using plaintext password format')
|
|
124
|
-
encPassword = this.plaintextPassword(password)
|
|
125
|
-
}
|
|
126
|
-
} catch (err) {
|
|
127
|
-
this.debugLog?.(`encryption failed: ${err}, falling back to plaintext`)
|
|
128
|
-
encPassword = this.plaintextPassword(password)
|
|
235
|
+
if (!encryptionKey) {
|
|
236
|
+
throw new InstagramError(
|
|
237
|
+
'Instagram did not return a password encryption key. Login cannot proceed safely.',
|
|
238
|
+
'encryption_key_missing',
|
|
239
|
+
)
|
|
129
240
|
}
|
|
130
241
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
242
|
+
this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`)
|
|
243
|
+
const encPassword = this.encryptPassword(password, encryptionKey)
|
|
244
|
+
|
|
245
|
+
const { status, data } = await this.request(
|
|
246
|
+
'POST',
|
|
247
|
+
'/accounts/login/',
|
|
248
|
+
{
|
|
249
|
+
username,
|
|
250
|
+
enc_password: encPassword,
|
|
251
|
+
guid: device.uuid,
|
|
252
|
+
phone_id: device.phone_id,
|
|
253
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
254
|
+
device_id: device.android_device_id,
|
|
255
|
+
adid: device.advertising_id,
|
|
256
|
+
google_tokens: '[]',
|
|
257
|
+
login_attempt_count: '0',
|
|
258
|
+
country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
|
|
259
|
+
jazoest: createJazoest(device.phone_id),
|
|
260
|
+
},
|
|
261
|
+
{ signed: true },
|
|
262
|
+
)
|
|
139
263
|
|
|
140
264
|
this.debugLog?.(`login response status=${status} body=${JSON.stringify(data)}`)
|
|
141
265
|
|
|
@@ -156,6 +280,25 @@ export class InstagramClient {
|
|
|
156
280
|
return { userId: '', challengeRequired: true, challengePath: challengeApiPath }
|
|
157
281
|
}
|
|
158
282
|
|
|
283
|
+
if (this.isFacebookLinkedLogin(data)) {
|
|
284
|
+
throw new InstagramError(
|
|
285
|
+
'This account appears to sign in with Facebook and has no usable Instagram password for CLI login. ' +
|
|
286
|
+
'Most reliable fix: log in to instagram.com in your browser, then run "agent-instagram auth extract". ' +
|
|
287
|
+
'Alternatively, give the account its own password. Most reliably, unlink Facebook first: in the Instagram app go to ' +
|
|
288
|
+
'Menu > Settings and privacy > Accounts Center > Manage accounts > (your Facebook profile) Manage > ' +
|
|
289
|
+
'Move out of this Account Center > Move account > Continue; removing it prompts you to create an Instagram password. ' +
|
|
290
|
+
'If Facebook is already unlinked, set one directly under ' +
|
|
291
|
+
'Menu > Settings and privacy > Accounts Center > Password and security > Change password. ' +
|
|
292
|
+
'Setting a password may enable "auth login", though Instagram can still reject automated logins from a new device.',
|
|
293
|
+
'facebook_linked',
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (this.hasLoginButton(data, 'send_one_click_login_email')) {
|
|
298
|
+
this.session.cookies = this.serializeCookies()
|
|
299
|
+
return { userId: '', oneClickEmailAvailable: true }
|
|
300
|
+
}
|
|
301
|
+
|
|
159
302
|
if (status !== 200 || data['status'] !== 'ok') {
|
|
160
303
|
const message = (data['message'] as string) ?? 'Login failed'
|
|
161
304
|
throw new InstagramError(message, errorType ?? 'login_failed')
|
|
@@ -166,14 +309,32 @@ export class InstagramClient {
|
|
|
166
309
|
}
|
|
167
310
|
|
|
168
311
|
async twoFactorLogin(username: string, code: string, twoFactorIdentifier: string): Promise<{ userId: string }> {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
312
|
+
const device = this.session?.device
|
|
313
|
+
const { status, data } = await this.request(
|
|
314
|
+
'POST',
|
|
315
|
+
'/accounts/two_factor_login/',
|
|
316
|
+
{
|
|
317
|
+
username,
|
|
318
|
+
verification_code: code,
|
|
319
|
+
two_factor_identifier: twoFactorIdentifier,
|
|
320
|
+
trust_this_device: '1',
|
|
321
|
+
verification_method: '3',
|
|
322
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
323
|
+
guid: device?.uuid ?? '',
|
|
324
|
+
phone_id: device?.phone_id ?? '',
|
|
325
|
+
device_id: device?.android_device_id ?? '',
|
|
326
|
+
},
|
|
327
|
+
{ signed: true },
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if (this.isBloksTwoFactorFallback(status, data)) {
|
|
331
|
+
this.debugLog?.('legacy two_factor_login rejected, retrying via Bloks flow')
|
|
332
|
+
const bloksContext =
|
|
333
|
+
typeof data['two_step_verification_context'] === 'string'
|
|
334
|
+
? data['two_step_verification_context']
|
|
335
|
+
: twoFactorIdentifier
|
|
336
|
+
return this.bloksTwoFactorLogin(code, bloksContext)
|
|
337
|
+
}
|
|
177
338
|
|
|
178
339
|
if (status !== 200 || data['status'] !== 'ok') {
|
|
179
340
|
const message = (data['message'] as string) ?? 'Two-factor authentication failed'
|
|
@@ -184,6 +345,157 @@ export class InstagramClient {
|
|
|
184
345
|
return { userId: this.userId ?? '' }
|
|
185
346
|
}
|
|
186
347
|
|
|
348
|
+
// The login dialog's "send_one_click_login_email" button triggers /accounts/one_click_login/
|
|
349
|
+
// with auto_send=true (NOT /accounts/send_recovery_flow_email/, which is the generic password
|
|
350
|
+
// reset). It needs the account's numeric user id, resolved via a pre-login /users/lookup/.
|
|
351
|
+
async sendOneClickLoginEmail(username: string): Promise<{ sent: boolean; contactPoint: string }> {
|
|
352
|
+
const device = await this.ensureDeviceSession()
|
|
353
|
+
const uid = await this.lookupUserId(username)
|
|
354
|
+
|
|
355
|
+
this.debugLog?.(`one_click_login auto_send for uid=${uid}`)
|
|
356
|
+
const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
|
|
357
|
+
uid,
|
|
358
|
+
source: 'one_click_login_email',
|
|
359
|
+
auto_send: 'true',
|
|
360
|
+
guid: device.uuid,
|
|
361
|
+
device_id: device.android_device_id,
|
|
362
|
+
adid: device.advertising_id,
|
|
363
|
+
})
|
|
364
|
+
this.debugLog?.(`one_click_login response status=${status} body=${JSON.stringify(data)}`)
|
|
365
|
+
|
|
366
|
+
if (status !== 200 || data['status'] === 'fail') {
|
|
367
|
+
const message = (data['message'] as string) ?? 'Failed to send login email'
|
|
368
|
+
throw new InstagramError(message, 'one_click_email_failed')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const contactPoint =
|
|
372
|
+
(data['obfuscated_email'] as string) ?? (data['email'] as string) ?? (data['contact_point'] as string) ?? ''
|
|
373
|
+
return { sent: true, contactPoint }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async lookupUserId(username: string): Promise<string> {
|
|
377
|
+
const device = await this.ensureDeviceSession()
|
|
378
|
+
|
|
379
|
+
this.debugLog?.(`users/lookup for ${username}`)
|
|
380
|
+
const { status, data } = await this.request(
|
|
381
|
+
'POST',
|
|
382
|
+
'/users/lookup/',
|
|
383
|
+
{
|
|
384
|
+
q: username,
|
|
385
|
+
directly_sign_in: 'true',
|
|
386
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
387
|
+
guid: device.uuid,
|
|
388
|
+
device_id: device.android_device_id,
|
|
389
|
+
country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
|
|
390
|
+
},
|
|
391
|
+
{ signed: true },
|
|
392
|
+
)
|
|
393
|
+
this.debugLog?.(`users/lookup response status=${status} body=${JSON.stringify(data)}`)
|
|
394
|
+
|
|
395
|
+
const userId = (data['user_id'] ?? data['uid'] ?? data['pk']) as string | number | undefined
|
|
396
|
+
if (userId == null || String(userId).length === 0) {
|
|
397
|
+
const message = (data['message'] as string) ?? `Could not find an Instagram account for "${username}".`
|
|
398
|
+
throw new InstagramError(message, 'user_lookup_failed')
|
|
399
|
+
}
|
|
400
|
+
return String(userId)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async oneClickLogin(uid: string, token: string, source = 'one_click_login_email'): Promise<{ userId: string }> {
|
|
404
|
+
const device = await this.ensureDeviceSession()
|
|
405
|
+
|
|
406
|
+
const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
|
|
407
|
+
uid,
|
|
408
|
+
token,
|
|
409
|
+
source,
|
|
410
|
+
device_id: device.android_device_id,
|
|
411
|
+
guid: device.uuid,
|
|
412
|
+
adid: device.advertising_id,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
|
|
416
|
+
const message = (data['message'] as string) ?? 'One-click login failed'
|
|
417
|
+
throw new InstagramError(message, 'one_click_login_failed')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!data['logged_in_user']) {
|
|
421
|
+
throw new InstagramError('One-click login did not return a session', 'one_click_login_failed')
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await this.finalizeLogin(data)
|
|
425
|
+
return { userId: this.userId ?? '' }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private async ensureDeviceSession(): Promise<InstagramDevice> {
|
|
429
|
+
if (!this.session) {
|
|
430
|
+
this.session = { cookies: '', device: await this.resolveDevice() }
|
|
431
|
+
}
|
|
432
|
+
return this.session.device
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private isFacebookLinkedLogin(data: Record<string, unknown>): boolean {
|
|
436
|
+
return this.hasLoginButton(data, 'login_with_facebook')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private hasLoginButton(data: Record<string, unknown>, action: string): boolean {
|
|
440
|
+
const buttons = data['buttons']
|
|
441
|
+
if (!Array.isArray(buttons)) return false
|
|
442
|
+
return buttons.some((button) => (button as Record<string, unknown>)?.['action'] === action)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private isBloksTwoFactorFallback(status: number, data: Record<string, unknown>): boolean {
|
|
446
|
+
if (status === 200 && data['status'] === 'ok') return false
|
|
447
|
+
const message = ((data['message'] as string) ?? '').trim().toLowerCase()
|
|
448
|
+
return message === 'invalid parameters' || data['two_step_verification_context'] != null
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private async bloksTwoFactorLogin(code: string, twoFactorIdentifier: string): Promise<{ userId: string }> {
|
|
452
|
+
const device = this.session?.device
|
|
453
|
+
const params = {
|
|
454
|
+
client_input_params: {
|
|
455
|
+
code,
|
|
456
|
+
should_trust_device: 1,
|
|
457
|
+
family_device_id: device?.phone_id ?? '',
|
|
458
|
+
device_id: device?.android_device_id ?? '',
|
|
459
|
+
machine_id: this.session?.mid ?? '',
|
|
460
|
+
},
|
|
461
|
+
server_params: {
|
|
462
|
+
challenge: 'totp',
|
|
463
|
+
two_step_verification_context: twoFactorIdentifier,
|
|
464
|
+
flow_source: 'two_factor_login',
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const { status, data } = await this.request(
|
|
469
|
+
'POST',
|
|
470
|
+
'/bloks/async_action/com.bloks.www.two_step_verification.verify_code.async/',
|
|
471
|
+
{ params: JSON.stringify(params), bk_client_context: JSON.stringify({ bloks_version: IG_BLOKS_VERSION_ID }) },
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
|
|
475
|
+
const message = (data['message'] as string) ?? 'Two-factor authentication failed'
|
|
476
|
+
throw new InstagramError(message, 'two_factor_failed')
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const loggedInUser = data['logged_in_user'] as Record<string, unknown> | undefined
|
|
480
|
+
if (loggedInUser) {
|
|
481
|
+
await this.finalizeLogin(data)
|
|
482
|
+
return { userId: this.userId ?? '' }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (this.session?.authorization) {
|
|
486
|
+
const userId = this.session.user_id ?? this.cookies.get('ds_user_id')
|
|
487
|
+
if (!userId) {
|
|
488
|
+
throw new InstagramError('Two-factor authentication failed', 'two_factor_failed')
|
|
489
|
+
}
|
|
490
|
+
this.userId = userId
|
|
491
|
+
this.session.user_id = userId
|
|
492
|
+
await this.saveSession()
|
|
493
|
+
return { userId }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
throw new InstagramError('Two-factor authentication failed', 'two_factor_failed')
|
|
497
|
+
}
|
|
498
|
+
|
|
187
499
|
async challengeSendCode(
|
|
188
500
|
apiPath: string,
|
|
189
501
|
method: 'email' | 'sms' = 'email',
|
|
@@ -450,16 +762,28 @@ export class InstagramClient {
|
|
|
450
762
|
const url = `${IG_BASE_URL}/qe/sync/`
|
|
451
763
|
const headers = this.buildHeaders()
|
|
452
764
|
|
|
453
|
-
const response = await
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
765
|
+
const { response } = await fetchWithTimeout(
|
|
766
|
+
url,
|
|
767
|
+
{
|
|
768
|
+
method: 'POST',
|
|
769
|
+
headers,
|
|
770
|
+
body: new URLSearchParams(
|
|
771
|
+
signBody({
|
|
772
|
+
id: this.session!.device.uuid,
|
|
773
|
+
experiments:
|
|
774
|
+
'ig_android_fci,ig_android_device_detection_info_upload,ig_android_device_verification_fb_signup',
|
|
775
|
+
}),
|
|
776
|
+
).toString(),
|
|
777
|
+
},
|
|
778
|
+
'/qe/sync/',
|
|
779
|
+
)
|
|
461
780
|
this.extractResponseCookies(response.headers)
|
|
462
781
|
|
|
782
|
+
const wwwClaim = response.headers.get('x-ig-set-www-claim')
|
|
783
|
+
if (wwwClaim && this.session) {
|
|
784
|
+
this.session.www_claim = wwwClaim
|
|
785
|
+
}
|
|
786
|
+
|
|
463
787
|
const pubKeyHeader = response.headers.get('ig-set-password-encryption-pub-key')
|
|
464
788
|
const keyIdHeader = response.headers.get('ig-set-password-encryption-key-id')
|
|
465
789
|
|
|
@@ -487,8 +811,15 @@ export class InstagramClient {
|
|
|
487
811
|
const pubKeyPem = Buffer.from(key.publicKey, 'base64').toString('utf-8')
|
|
488
812
|
const rsaEncrypted = publicEncrypt({ key: pubKeyPem, padding: constants.RSA_PKCS1_PADDING }, aesKey)
|
|
489
813
|
|
|
814
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key.keyId)) {
|
|
815
|
+
throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid')
|
|
816
|
+
}
|
|
817
|
+
const keyIdNum = Number(key.keyId)
|
|
818
|
+
if (keyIdNum > 255) {
|
|
819
|
+
throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid')
|
|
820
|
+
}
|
|
821
|
+
|
|
490
822
|
// Wire format: version(1) | keyId(1) | iv(12) | rsaLen(2 LE) | rsaEncrypted | tag(16) | aesEncrypted
|
|
491
|
-
const keyIdNum = Number.parseInt(key.keyId, 10)
|
|
492
823
|
const buf = Buffer.alloc(1 + 1 + 12 + 2 + rsaEncrypted.length + 16 + encrypted.length)
|
|
493
824
|
let offset = 0
|
|
494
825
|
buf.writeUInt8(1, offset)
|
|
@@ -508,25 +839,22 @@ export class InstagramClient {
|
|
|
508
839
|
return `#PWD_INSTAGRAM:4:${timestamp}:${buf.toString('base64')}`
|
|
509
840
|
}
|
|
510
841
|
|
|
511
|
-
private plaintextPassword(password: string): string {
|
|
512
|
-
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
513
|
-
return `#PWD_INSTAGRAM:0:${timestamp}:${password}`
|
|
514
|
-
}
|
|
515
|
-
|
|
516
842
|
private async request(
|
|
517
843
|
method: string,
|
|
518
844
|
path: string,
|
|
519
845
|
body?: Record<string, string>,
|
|
846
|
+
options?: { signed?: boolean },
|
|
520
847
|
): Promise<{ status: number; data: Record<string, unknown> }> {
|
|
521
848
|
const url = `${IG_BASE_URL}${path}`
|
|
522
849
|
const headers = this.buildHeaders()
|
|
523
850
|
|
|
524
|
-
const
|
|
851
|
+
const requestInit: RequestInit = { method, headers }
|
|
525
852
|
if (body) {
|
|
526
|
-
|
|
853
|
+
const payload = options?.signed ? signBody(body) : body
|
|
854
|
+
requestInit.body = new URLSearchParams(payload).toString()
|
|
527
855
|
}
|
|
528
856
|
|
|
529
|
-
const response = await
|
|
857
|
+
const { response, readText } = await fetchWithTimeout(url, requestInit, path)
|
|
530
858
|
this.extractResponseCookies(response.headers)
|
|
531
859
|
|
|
532
860
|
const authHeader = response.headers.get('ig-set-authorization')
|
|
@@ -534,13 +862,27 @@ export class InstagramClient {
|
|
|
534
862
|
this.session.authorization = authHeader
|
|
535
863
|
}
|
|
536
864
|
|
|
865
|
+
const wwwClaim = response.headers.get('x-ig-set-www-claim')
|
|
866
|
+
if (wwwClaim && this.session) {
|
|
867
|
+
this.session.www_claim = wwwClaim
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const rawBody = await readText()
|
|
871
|
+
|
|
537
872
|
if (response.status === 429) {
|
|
538
|
-
|
|
873
|
+
this.debugLog?.(`429 from ${path} body=${rawBody}`)
|
|
874
|
+
const igMessage = this.extractJsonMessage(rawBody)
|
|
875
|
+
throw new InstagramError(
|
|
876
|
+
igMessage
|
|
877
|
+
? `Rate limited by Instagram: ${igMessage} Wait before trying again.`
|
|
878
|
+
: 'Rate limited by Instagram. Try again later.',
|
|
879
|
+
'rate_limited',
|
|
880
|
+
)
|
|
539
881
|
}
|
|
540
882
|
|
|
541
883
|
let data: Record<string, unknown>
|
|
542
884
|
try {
|
|
543
|
-
data =
|
|
885
|
+
data = JSON.parse(rawBody) as Record<string, unknown>
|
|
544
886
|
} catch {
|
|
545
887
|
throw new InstagramError(`Failed to parse response from ${path}`, response.status)
|
|
546
888
|
}
|
|
@@ -548,22 +890,51 @@ export class InstagramClient {
|
|
|
548
890
|
return { status: response.status, data }
|
|
549
891
|
}
|
|
550
892
|
|
|
893
|
+
private extractJsonMessage(body: string): string {
|
|
894
|
+
try {
|
|
895
|
+
const parsed = JSON.parse(body) as Record<string, unknown>
|
|
896
|
+
return (parsed['message'] as string) ?? ''
|
|
897
|
+
} catch {
|
|
898
|
+
return ''
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
551
902
|
private buildHeaders(): Record<string, string> {
|
|
552
903
|
const deviceString = this.session?.device.device_string ?? generateDeviceString()
|
|
904
|
+
// Instagram's edge silently drops connections whose fingerprint headers don't match a real
|
|
905
|
+
// Android client. This is a partial alignment toward the current app profile
|
|
906
|
+
// (per subzeroid/instagrapi) covering the highest-signal headers, not a full fingerprint match
|
|
907
|
+
// (e.g. X-Tigon-Is-Retry, X-Zero-*, X-IG-Nav-Chain, X-IG-SALT-IDS are intentionally omitted).
|
|
553
908
|
const headers: Record<string, string> = {
|
|
554
909
|
'User-Agent': buildUserAgent(deviceString),
|
|
910
|
+
'X-IG-App-Locale': IG_LOCALE,
|
|
911
|
+
'X-IG-Device-Locale': IG_LOCALE,
|
|
912
|
+
'X-IG-Mapped-Locale': IG_LOCALE,
|
|
913
|
+
'X-Pigeon-Session-Id': `UFS-${randomUUID()}-0`,
|
|
914
|
+
'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3),
|
|
915
|
+
'X-IG-Bandwidth-Speed-KBPS': '-1.000',
|
|
916
|
+
'X-IG-Bandwidth-TotalBytes-B': '0',
|
|
917
|
+
'X-IG-Bandwidth-TotalTime-MS': '0',
|
|
918
|
+
'X-IG-App-Startup-Country': 'US',
|
|
555
919
|
'X-IG-App-ID': IG_APP_ID,
|
|
556
|
-
'X-IG-Capabilities':
|
|
920
|
+
'X-IG-Capabilities': IG_CAPABILITIES,
|
|
557
921
|
'X-IG-Connection-Type': 'WIFI',
|
|
558
922
|
'X-IG-Timezone-Offset': String(new Date().getTimezoneOffset() * -60),
|
|
923
|
+
'X-Bloks-Version-Id': IG_BLOKS_VERSION_ID,
|
|
924
|
+
'X-FB-HTTP-Engine': 'Tigon/MNS/TCP',
|
|
925
|
+
Priority: 'u=3',
|
|
926
|
+
'IG-INTENDED-USER-ID': this.userId ?? '0',
|
|
559
927
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
560
928
|
Accept: '*/*',
|
|
561
929
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
562
930
|
}
|
|
563
931
|
|
|
932
|
+
headers['X-IG-WWW-Claim'] = this.session?.www_claim ?? '0'
|
|
933
|
+
|
|
564
934
|
if (this.session) {
|
|
565
935
|
headers['X-IG-Device-ID'] = this.session.device.uuid
|
|
566
936
|
headers['X-IG-Android-ID'] = this.session.device.android_device_id
|
|
937
|
+
headers['X-IG-Family-Device-ID'] = this.session.device.phone_id
|
|
567
938
|
if (this.session.authorization) {
|
|
568
939
|
headers['Authorization'] = this.session.authorization
|
|
569
940
|
}
|
|
@@ -636,6 +1007,16 @@ export class InstagramClient {
|
|
|
636
1007
|
await this.saveSession()
|
|
637
1008
|
}
|
|
638
1009
|
|
|
1010
|
+
// Reuse one persisted device per machine so repeat logins present a consistent fingerprint.
|
|
1011
|
+
// Regenerating device ids each attempt looks like a brand-new phone and hurts login trust.
|
|
1012
|
+
private async resolveDevice(): Promise<InstagramDevice> {
|
|
1013
|
+
const existing = await this.credentialManager.loadDevice()
|
|
1014
|
+
if (existing) return existing
|
|
1015
|
+
const device = generateDevice()
|
|
1016
|
+
await this.credentialManager.saveDevice(device)
|
|
1017
|
+
return device
|
|
1018
|
+
}
|
|
1019
|
+
|
|
639
1020
|
private async saveSession(): Promise<void> {
|
|
640
1021
|
if (!this.session || !this.sessionPath) return
|
|
641
1022
|
this.session.cookies = this.serializeCookies()
|