@xmanrui/dsh-im 0.15.0 → 0.16.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.
@@ -21,7 +21,16 @@ const REGISTRATION_STATES = new Set([
21
21
  'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
22
22
  'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
23
23
  ]);
24
+ const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
25
+ const CALLBACK_REPAIR_OPERATION = 'callback_repair';
26
+ const OFFICIAL_REGISTRATION_HOSTS = new Set([
27
+ 'accounts.feishu.cn',
28
+ 'accounts.larksuite.com',
29
+ 'open.feishu.cn',
30
+ 'open.larksuite.com',
31
+ ]);
24
32
  const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
33
+ const SAFE_FEISHU_APP_ID = /^cli_[A-Za-z0-9_-]+$/;
25
34
 
26
35
  const PUBLIC_ERROR_MESSAGES = Object.freeze({
27
36
  abort: 'Registration was cancelled.',
@@ -35,6 +44,20 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
35
44
  state_cleanup_failed: 'Unable to remove the bot session data. Please retry.',
36
45
  deletion_pending: 'Bot deletion is incomplete. Retry removal to finish cleanup.',
37
46
  missing_credentials: 'The bot credentials are missing. Delete it and scan again.',
47
+ repair_app_mismatch: 'The authorized Feishu app does not match the selected bot.',
48
+ repair_owner_missing: 'Feishu did not return the authorizing account identity.',
49
+ repair_domain_mismatch: 'The authorized Feishu tenant does not match the selected bot.',
50
+ repair_owner_mismatch: 'The authorizing Feishu account is not an owner of the selected bot.',
51
+ repair_credentials_invalid: 'Feishu returned credentials that could not be verified for the selected bot.',
52
+ repair_bot_mismatch: 'The verified Feishu bot does not match the selected bot.',
53
+ repair_target_changed: 'The selected bot changed while repair was in progress. Start the repair again.',
54
+ credential_update_failed: 'Unable to store the repaired Feishu credentials.',
55
+ credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
56
+ repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
57
+ card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
58
+ card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
59
+ card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
60
+ card_action_probe_failed: 'Feishu accepted the update, but the card button verification failed.',
38
61
  });
39
62
 
40
63
  const POLL_STATUS_BY_REGISTRATION = Object.freeze({
@@ -81,6 +104,9 @@ function publicRegistration(registration) {
81
104
  ? registration.attempt
82
105
  : (finiteNumber(registration.attempt) ?? 0);
83
106
  const result = { state, attempt };
107
+ result.operation = REGISTRATION_OPERATIONS.has(registration.operation)
108
+ ? registration.operation
109
+ : 'provision';
84
110
  const updatedAt = finiteNumber(registration.updatedAt);
85
111
  const expiresAt = finiteNumber(registration.expiresAt);
86
112
  const remainingSeconds = finiteNumber(registration.remainingSeconds);
@@ -98,6 +124,40 @@ function publicRegistration(registration) {
98
124
  return result;
99
125
  }
100
126
 
127
+ function safeRegistrationUrl(value, operation, expectedHost) {
128
+ if (typeof value !== 'string' || value.length === 0) return undefined;
129
+ try {
130
+ const url = new URL(value);
131
+ if (url.protocol !== 'https:'
132
+ || !OFFICIAL_REGISTRATION_HOSTS.has(url.hostname)
133
+ || url.port
134
+ || url.username
135
+ || url.password) return undefined;
136
+ if (operation === CALLBACK_REPAIR_OPERATION) {
137
+ const clientIds = url.searchParams.getAll('clientID');
138
+ const transportKinds = url.searchParams.getAll('tp');
139
+ const addons = url.searchParams.getAll('addons');
140
+ const hasPlaceholder = [...url.searchParams.values()].some((item) => (
141
+ item.includes('{{') || item.includes('}}')
142
+ ));
143
+ if (clientIds.length !== 1
144
+ || transportKinds.length !== 1
145
+ || transportKinds[0] !== 'sdk'
146
+ || !SAFE_FEISHU_APP_ID.test(clientIds[0] ?? '')
147
+ || url.hostname !== expectedHost
148
+ || url.searchParams.has('createOnly')
149
+ || addons.length !== 1
150
+ || !addons[0]?.trim()
151
+ || hasPlaceholder) {
152
+ return undefined;
153
+ }
154
+ }
155
+ return url.toString();
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ }
160
+
101
161
  function connectionFacts(connection) {
102
162
  const source = connection && typeof connection === 'object' ? connection : {};
103
163
  const connected = source.connected === true
@@ -151,15 +211,35 @@ async function qrCodeDataUrl(verificationUrl) {
151
211
  });
152
212
  }
153
213
 
154
- async function publicProvisioning(registration, encodeQr) {
155
- if (!registration.qrCodeUrl) return undefined;
156
- return {
214
+ async function publicProvisioning(registration, encodeQr, expectedHost) {
215
+ const verificationUrl = safeRegistrationUrl(
216
+ registration.qrCodeUrl,
217
+ registration.operation,
218
+ expectedHost,
219
+ );
220
+ const projection = {
157
221
  attemptId: String(registration.attempt),
158
- verificationUrl: registration.qrCodeUrl,
159
- qrCodeDataUrl: await encodeQr(registration.qrCodeUrl),
222
+ operation: registration.operation,
223
+ ...(registration.botId ? { botId: registration.botId } : {}),
160
224
  expiresAt: registration.expiresAt ?? Date.now() + (5 * 60_000),
161
225
  pollIntervalMs: Math.max(800, Math.min(10_000, (registration.pollIntervalSeconds ?? 1.8) * 1000)),
162
226
  };
227
+ if (verificationUrl) {
228
+ return {
229
+ ...projection,
230
+ verificationUrl,
231
+ qrCodeDataUrl: await encodeQr(verificationUrl),
232
+ };
233
+ }
234
+ // RegistrationManager deliberately removes the device URL as soon as the
235
+ // remote update is committed. Keep only the opaque attempt identity so a
236
+ // browser reload can resume polling the non-cancellable verification phase.
237
+ if (registration.state === 'saving'
238
+ && registration.operation === CALLBACK_REPAIR_OPERATION
239
+ && registration.botId) {
240
+ return { ...projection, submitted: true };
241
+ }
242
+ return undefined;
163
243
  }
164
244
 
165
245
  function publicBotEntry(entry) {
@@ -188,7 +268,19 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
188
268
  const registration = publicRegistration(source.registration);
189
269
  const facts = connectionFacts(source.connection);
190
270
  const connected = source.connected === true || facts.connected;
191
- const provisioning = await publicProvisioning(registration, encodeQr);
271
+ const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
272
+ && registration.botId
273
+ && Array.isArray(source.bots)
274
+ ? source.bots.find((entry) => entry?.botId === registration.botId)
275
+ : undefined;
276
+ const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
277
+ ? 'open.larksuite.com'
278
+ : 'open.feishu.cn';
279
+ const provisioning = await publicProvisioning(
280
+ registration,
281
+ encodeQr,
282
+ expectedRegistrationHost,
283
+ );
192
284
  const error = publicError(source.error) ?? registration.error ?? null;
193
285
  const bots = Array.isArray(source.bots)
194
286
  ? source.bots.map(publicBotEntry).filter(Boolean)
@@ -241,6 +333,11 @@ function validPayload(endpoint, payload) {
241
333
  }
242
334
  return null;
243
335
  }
336
+ if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) {
337
+ return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
338
+ ? null
339
+ : 'Callback repair requires a single valid botId.';
340
+ }
244
341
  if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
245
342
  return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
246
343
  && validCredential(payload.appId, 256)
@@ -386,6 +483,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
386
483
  value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
387
484
  if (!value) throw new Error('Provisioning did not produce a QR code.');
388
485
  attemptQr.set(attemptId, value.verificationUrl);
486
+ } else if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) {
487
+ if (typeof controller.startCallbackRepair !== 'function') {
488
+ throw new Error('Feishu callback repair is unavailable.');
489
+ }
490
+ const started = await controller.startCallbackRepair(payload.botId);
491
+ const attemptId = String(publicRegistration(started?.registration).attempt);
492
+ const ready = await waitForQr(controller, started, attemptId, signal);
493
+ value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
494
+ if (!value
495
+ || value.operation !== CALLBACK_REPAIR_OPERATION
496
+ || value.botId !== payload.botId) {
497
+ throw new Error('Callback repair did not produce a safe QR code.');
498
+ }
499
+ attemptQr.set(attemptId, value.verificationUrl);
389
500
  } else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
390
501
  const current = await statusForRegistration(controller, payload.attemptId);
391
502
  if (!current || !sameAttempt(current, payload.attemptId)) {
@@ -395,6 +506,7 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
395
506
  const connection = await toPublicFeishuStatus(current, { encodeQr: cachedEncodeQr });
396
507
  value = {
397
508
  status: pollStatus(current),
509
+ operation: registration.operation,
398
510
  ...(registration.botId ? { botId: registration.botId } : {}),
399
511
  ...(connection.provisioning ? { provisioning: connection.provisioning } : {}),
400
512
  ...(registration.botId && connection.connected ? { connection } : {}),
@@ -412,12 +524,34 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
412
524
  }
413
525
  const multi = typeof controller.registrationStatus === 'function';
414
526
  const registration = publicRegistration(current.registration);
415
- if (!multi && registration.state === 'saving') await controller.disconnect();
416
- else await controller.cancelRegistration(payload.attemptId);
417
- const url = attemptQr.get(payload.attemptId);
418
- if (url) qrCache.delete(url);
419
- attemptQr.delete(payload.attemptId);
420
- value = { status: 'failed', message: 'Registration was cancelled.' };
527
+ let after;
528
+ if (!multi && registration.state === 'saving') {
529
+ after = await controller.disconnect();
530
+ } else {
531
+ after = await controller.cancelRegistration(payload.attemptId);
532
+ }
533
+ const afterRegistration = publicRegistration(after?.registration);
534
+ if (registration.operation === CALLBACK_REPAIR_OPERATION
535
+ && registration.state === 'saving'
536
+ && ['saving', 'succeeded'].includes(afterRegistration.state)) {
537
+ value = {
538
+ status: pollStatus(after),
539
+ operation: afterRegistration.operation,
540
+ ...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
541
+ message: 'Callback repair was already submitted and is still being verified.',
542
+ };
543
+ }
544
+ if (value?.status !== 'connecting') {
545
+ const url = attemptQr.get(payload.attemptId);
546
+ if (url) qrCache.delete(url);
547
+ attemptQr.delete(payload.attemptId);
548
+ value ??= {
549
+ status: 'failed',
550
+ operation: registration.operation,
551
+ ...(registration.botId ? { botId: registration.botId } : {}),
552
+ message: 'Registration was cancelled.',
553
+ };
554
+ }
421
555
  } else if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
422
556
  if (typeof controller.bindCredentials !== 'function') {
423
557
  throw new Error('Credential binding is unavailable');
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.15.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.16.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),