@botcord/daemon 0.2.38 → 0.2.41

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.
@@ -72,17 +72,18 @@ export function createTelegramChannel(opts) {
72
72
  }
73
73
  return botToken;
74
74
  }
75
- async function callApi(method, params, timeoutMs) {
75
+ async function callApi(method, params, timeoutMs, abortSignal) {
76
76
  if (!botToken)
77
77
  throw new Error("telegram bot token not loaded");
78
78
  const url = `${baseUrl}/bot${botToken}/${method}`;
79
+ const signalLease = createTimeoutSignal(timeoutMs, abortSignal);
79
80
  let resp;
80
81
  try {
81
82
  resp = await fetchImpl(url, {
82
83
  method: "POST",
83
84
  headers: { "Content-Type": "application/json" },
84
85
  body: JSON.stringify(params),
85
- signal: AbortSignal.timeout(timeoutMs),
86
+ signal: signalLease.signal,
86
87
  });
87
88
  }
88
89
  catch (err) {
@@ -94,6 +95,9 @@ export function createTelegramChannel(opts) {
94
95
  next.name = e.name ?? "Error";
95
96
  throw next;
96
97
  }
98
+ finally {
99
+ signalLease.cleanup();
100
+ }
97
101
  const json = (await resp.json());
98
102
  return json;
99
103
  }
@@ -156,7 +160,7 @@ export function createTelegramChannel(opts) {
156
160
  replyTo: null,
157
161
  mentioned: false,
158
162
  receivedAt: Date.now(),
159
- trace: { id: messageId, streamable: false },
163
+ trace: { id: messageId, streamable: true },
160
164
  };
161
165
  }
162
166
  async function pollLoop(ctx) {
@@ -194,20 +198,20 @@ export function createTelegramChannel(opts) {
194
198
  });
195
199
  log.info("telegram poll loop starting", { gatewayId: opts.id, offset });
196
200
  let stopped = false;
201
+ const stopController = new AbortController();
197
202
  const onAbort = () => {
198
203
  stopped = true;
204
+ stopController.abort();
199
205
  };
200
206
  abortSignal.addEventListener("abort", onAbort, { once: true });
201
- stopCallback = () => {
202
- stopped = true;
203
- };
207
+ stopCallback = onAbort;
204
208
  while (!stopped && !abortSignal.aborted) {
205
209
  try {
206
210
  const resp = await callApi("getUpdates", {
207
211
  offset,
208
212
  timeout: POLL_TIMEOUT_S,
209
213
  allowed_updates: ["message"],
210
- }, (POLL_TIMEOUT_S + 15) * 1000);
214
+ }, (POLL_TIMEOUT_S + 15) * 1000, stopController.signal);
211
215
  markStatus({ lastPollAt: Date.now() });
212
216
  if (!resp.ok) {
213
217
  log.warn("telegram getUpdates non-ok", {
@@ -369,3 +373,38 @@ function sleep(ms, signal) {
369
373
  signal?.addEventListener("abort", onAbort, { once: true });
370
374
  });
371
375
  }
376
+ function createTimeoutSignal(timeoutMs, parent) {
377
+ const controller = new AbortController();
378
+ let settled = false;
379
+ const abort = (reason) => {
380
+ if (settled)
381
+ return;
382
+ settled = true;
383
+ try {
384
+ controller.abort(reason);
385
+ }
386
+ catch {
387
+ controller.abort();
388
+ }
389
+ };
390
+ const timer = setTimeout(() => {
391
+ abort(new DOMException("Timeout", "TimeoutError"));
392
+ }, timeoutMs);
393
+ const onParentAbort = () => abort(parent?.reason);
394
+ if (parent) {
395
+ if (parent.aborted) {
396
+ onParentAbort();
397
+ }
398
+ else {
399
+ parent.addEventListener("abort", onParentAbort, { once: true });
400
+ }
401
+ }
402
+ return {
403
+ signal: controller.signal,
404
+ cleanup: () => {
405
+ clearTimeout(timer);
406
+ if (parent)
407
+ parent.removeEventListener("abort", onParentAbort);
408
+ },
409
+ };
410
+ }
@@ -591,6 +591,7 @@ export class Dispatcher {
591
591
  const trustLevel = route.trustLevel ?? "trusted";
592
592
  const streamable = msg.trace?.streamable === true;
593
593
  const traceId = msg.trace?.id;
594
+ const canType = streamable && typeof traceId === "string" && typeof channel.typing === "function";
594
595
  const canStream = streamable && typeof traceId === "string" && typeof channel.streamBlock === "function";
595
596
  const recordBlock = (block) => {
596
597
  const summary = { type: block.kind };
@@ -670,7 +671,7 @@ export class Dispatcher {
670
671
  forwardBlockToChannel(synth);
671
672
  };
672
673
  const fireTypingIfNeeded = () => {
673
- if (!canStream || typingFired || typeof channel.typing !== "function")
674
+ if (!canType || typingFired)
674
675
  return;
675
676
  typingFired = true;
676
677
  const key = `${msg.accountId}:${msg.conversation.id}`;
@@ -8,13 +8,49 @@
8
8
  * gateway, login-session store, fetch impl, and config I/O — `provision.ts`
9
9
  * wires the production defaults.
10
10
  */
11
- import type { ControlAck, GatewayLoginStartParams, GatewayLoginStatusParams, RemoveGatewayParams, TestGatewayParams, UpsertGatewayParams } from "@botcord/protocol-core";
11
+ import type { ControlAck } from "@botcord/protocol-core";
12
12
  import type { Gateway } from "./gateway/index.js";
13
13
  import { type DaemonConfig } from "./config.js";
14
14
  import { LoginSessionStore } from "./gateway/channels/login-session.js";
15
15
  import { getBotQrcode, getQrcodeStatus } from "./gateway/channels/wechat-login.js";
16
16
  import type { FetchLike } from "./gateway/channels/http-types.js";
17
17
  type AckBody = Omit<ControlAck, "id">;
18
+ type GatewayProvider = "telegram" | "wechat";
19
+ interface UpsertGatewayParams {
20
+ id: string;
21
+ type: GatewayProvider;
22
+ accountId: string;
23
+ label?: string;
24
+ enabled?: boolean;
25
+ loginId?: string;
26
+ secret?: {
27
+ botToken?: string;
28
+ };
29
+ settings?: {
30
+ baseUrl?: string;
31
+ allowedSenderIds?: string[];
32
+ allowedChatIds?: string[];
33
+ splitAt?: number;
34
+ };
35
+ }
36
+ interface RemoveGatewayParams {
37
+ id: string;
38
+ deleteSecret?: boolean;
39
+ }
40
+ interface TestGatewayParams {
41
+ id: string;
42
+ }
43
+ interface GatewayLoginStartParams {
44
+ provider: GatewayProvider;
45
+ accountId: string;
46
+ gatewayId?: string;
47
+ baseUrl?: string;
48
+ }
49
+ interface GatewayLoginStatusParams {
50
+ provider: GatewayProvider;
51
+ loginId: string;
52
+ accountId: string;
53
+ }
18
54
  export type { FetchLike };
19
55
  export interface GatewayControlContext {
20
56
  gateway: Gateway;
package/dist/provision.js CHANGED
@@ -201,9 +201,9 @@ export function createProvisioner(opts) {
201
201
  daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
202
202
  return { ok: true, result: snapshot };
203
203
  }
204
- case CONTROL_FRAME_TYPES.LIST_GATEWAYS:
204
+ case "list_gateways":
205
205
  return gatewayControl.handleList();
206
- case CONTROL_FRAME_TYPES.UPSERT_GATEWAY: {
206
+ case "upsert_gateway": {
207
207
  const v = validateGatewayParams(frame.params, {
208
208
  required: ["id", "type", "accountId"],
209
209
  });
@@ -211,19 +211,19 @@ export function createProvisioner(opts) {
211
211
  return v.ack;
212
212
  return gatewayControl.handleUpsert(v.params);
213
213
  }
214
- case CONTROL_FRAME_TYPES.REMOVE_GATEWAY: {
214
+ case "remove_gateway": {
215
215
  const v = validateGatewayParams(frame.params, { required: ["id"] });
216
216
  if (!v.ok)
217
217
  return v.ack;
218
218
  return gatewayControl.handleRemove(v.params);
219
219
  }
220
- case CONTROL_FRAME_TYPES.TEST_GATEWAY: {
220
+ case "test_gateway": {
221
221
  const v = validateGatewayParams(frame.params, { required: ["id"] });
222
222
  if (!v.ok)
223
223
  return v.ack;
224
224
  return gatewayControl.handleTest(v.params);
225
225
  }
226
- case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START: {
226
+ case "gateway_login_start": {
227
227
  const v = validateGatewayParams(frame.params, {
228
228
  required: ["provider", "accountId"],
229
229
  });
@@ -231,7 +231,7 @@ export function createProvisioner(opts) {
231
231
  return v.ack;
232
232
  return gatewayControl.handleLoginStart(v.params);
233
233
  }
234
- case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS: {
234
+ case "gateway_login_status": {
235
235
  const v = validateGatewayParams(frame.params, {
236
236
  required: ["provider", "loginId"],
237
237
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.38",
3
+ "version": "0.2.41",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@botcord/cli": "^0.1.7",
31
- "@botcord/protocol-core": "^0.2.2",
31
+ "@botcord/protocol-core": "^0.2.4",
32
32
  "ws": "^8.18.0"
33
33
  },
34
34
  "devDependencies": {
@@ -1698,7 +1698,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
1698
1698
  });
1699
1699
  const ack = await provisioner({
1700
1700
  id: "req_w8a",
1701
- type: CONTROL_FRAME_TYPES.UPSERT_GATEWAY,
1701
+ type: "upsert_gateway",
1702
1702
  params: { id: "gw_x", type: "telegram" } as unknown as Record<string, unknown>,
1703
1703
  });
1704
1704
  expect(ack.ok).toBe(false);
@@ -1714,7 +1714,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
1714
1714
  });
1715
1715
  const ack = await provisioner({
1716
1716
  id: "req_w8b",
1717
- type: CONTROL_FRAME_TYPES.REMOVE_GATEWAY,
1717
+ type: "remove_gateway",
1718
1718
  // @ts-expect-error — exercising the runtime guard
1719
1719
  params: "not-an-object",
1720
1720
  });
@@ -1729,7 +1729,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
1729
1729
  });
1730
1730
  const ack = await provisioner({
1731
1731
  id: "req_w8c",
1732
- type: CONTROL_FRAME_TYPES.TEST_GATEWAY,
1732
+ type: "test_gateway",
1733
1733
  params: {},
1734
1734
  });
1735
1735
  expect(ack.ok).toBe(false);
@@ -1743,7 +1743,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
1743
1743
  });
1744
1744
  const ack = await provisioner({
1745
1745
  id: "req_w8d",
1746
- type: CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START,
1746
+ type: "gateway_login_start",
1747
1747
  params: { accountId: "ag_a" },
1748
1748
  });
1749
1749
  expect(ack.ok).toBe(false);
@@ -1757,7 +1757,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
1757
1757
  });
1758
1758
  const ack = await provisioner({
1759
1759
  id: "req_w8e",
1760
- type: CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS,
1760
+ type: "gateway_login_status",
1761
1761
  params: { provider: "wechat" },
1762
1762
  });
1763
1763
  expect(ack.ok).toBe(false);
@@ -619,7 +619,7 @@ describe("Dispatcher", () => {
619
619
  // typing / thinking lifecycle (design: runtime-typing-thinking-status-design.md)
620
620
  // ---------------------------------------------------------------------------
621
621
 
622
- it("typing: fires channel.typing once before runtime.run when canStream", async () => {
622
+ it("typing: fires channel.typing once before runtime.run when trace is streamable", async () => {
623
623
  const observed: Array<{ typings: number; calls: number }> = [];
624
624
  const runtime = new FakeRuntime({
625
625
  observeRun: () => {
@@ -644,6 +644,21 @@ describe("Dispatcher", () => {
644
644
  expect(observed[0]?.typings).toBe(1);
645
645
  });
646
646
 
647
+ it("typing: does not require channel streamBlock support", async () => {
648
+ const runtime = new FakeRuntime({ reply: "ok", newSessionId: "sid" });
649
+ const channel = new FakeChannel({ withStream: false });
650
+ const { dispatcher } = await scaffold({ channel, runtimeFactory: () => runtime });
651
+
652
+ await dispatcher.handle(
653
+ makeEnvelope({ trace: { id: "trace_t", streamable: true } }),
654
+ );
655
+ await new Promise((r) => setTimeout(r, 5));
656
+
657
+ expect(channel.typings.length).toBe(1);
658
+ expect(channel.streams.length).toBe(0);
659
+ expect(channel.sends.length).toBe(1);
660
+ });
661
+
647
662
  it("typing: not fired when streamable is false", async () => {
648
663
  const channel = new FakeChannel();
649
664
  const { dispatcher } = await scaffold({
@@ -144,6 +144,7 @@ describe("createTelegramChannel — start()", () => {
144
144
  expect(msg.text).toBe("hello world");
145
145
  expect(msg.accountId).toBe("ag_self");
146
146
  expect(msg.channel).toBe("gw_tg_a");
147
+ expect(msg.trace).toEqual({ id: "telegram:42:7", streamable: true });
147
148
  });
148
149
 
149
150
  it("uses telegram:group:<id> for non-private chats", async () => {
@@ -319,6 +320,55 @@ describe("createTelegramChannel — start()", () => {
319
320
  expect(calls).toHaveLength(1);
320
321
  expect((calls[0]!.body as Record<string, unknown>).offset).toBe(999);
321
322
  });
323
+
324
+ it("aborts an in-flight getUpdates request when stopped", async () => {
325
+ const calls: FetchCall[] = [];
326
+ let requestSignal: AbortSignal | undefined;
327
+ let fetchStarted!: () => void;
328
+ const fetchStartedPromise = new Promise<void>((resolve) => {
329
+ fetchStarted = resolve;
330
+ });
331
+ const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
332
+ const url = typeof input === "string" ? input : input.toString();
333
+ const body = init?.body ? JSON.parse(init.body as string) : undefined;
334
+ calls.push({ url, body });
335
+ requestSignal = init?.signal ?? undefined;
336
+ fetchStarted();
337
+ return await new Promise<Response>((_resolve, reject) => {
338
+ requestSignal?.addEventListener(
339
+ "abort",
340
+ () => {
341
+ const err = new Error("aborted");
342
+ err.name = "AbortError";
343
+ reject(err);
344
+ },
345
+ { once: true },
346
+ );
347
+ });
348
+ }) as typeof fetch;
349
+
350
+ const channel = createTelegramChannel({
351
+ id: "gw_tg_stop",
352
+ accountId: "ag_self",
353
+ botToken: "tok",
354
+ allowedChatIds: ["42"],
355
+ allowedSenderIds: ["42"],
356
+ stateFile: path.join(tmp, "state.json"),
357
+ stateDebounceMs: 0,
358
+ fetchImpl,
359
+ });
360
+ const abort = new AbortController();
361
+ const { ctx } = makeStartCtx({ abort });
362
+ const startPromise = channel.start(ctx);
363
+
364
+ await fetchStartedPromise;
365
+ expect(requestSignal?.aborted).toBe(false);
366
+ await channel.stop!({ reason: "remove_gateway" });
367
+ await startPromise;
368
+
369
+ expect(requestSignal?.aborted).toBe(true);
370
+ expect(calls[0]!.url).toContain("/getUpdates");
371
+ });
322
372
  });
323
373
 
324
374
  describe("createTelegramChannel — send()", () => {
@@ -152,16 +152,18 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
152
152
  method: string,
153
153
  params: Record<string, unknown>,
154
154
  timeoutMs: number,
155
+ abortSignal?: AbortSignal,
155
156
  ): Promise<TelegramApiResult<T>> {
156
157
  if (!botToken) throw new Error("telegram bot token not loaded");
157
158
  const url = `${baseUrl}/bot${botToken}/${method}`;
159
+ const signalLease = createTimeoutSignal(timeoutMs, abortSignal);
158
160
  let resp: Response;
159
161
  try {
160
162
  resp = await fetchImpl(url, {
161
163
  method: "POST",
162
164
  headers: { "Content-Type": "application/json" },
163
165
  body: JSON.stringify(params),
164
- signal: AbortSignal.timeout(timeoutMs),
166
+ signal: signalLease.signal,
165
167
  });
166
168
  } catch (err) {
167
169
  // C3: fetch errors often stringify the URL (which embeds the token).
@@ -171,6 +173,8 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
171
173
  const next = new Error(redacted);
172
174
  next.name = e.name ?? "Error";
173
175
  throw next;
176
+ } finally {
177
+ signalLease.cleanup();
174
178
  }
175
179
  const json = (await resp.json()) as TelegramApiResult<T>;
176
180
  return json;
@@ -237,7 +241,7 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
237
241
  replyTo: null,
238
242
  mentioned: false,
239
243
  receivedAt: Date.now(),
240
- trace: { id: messageId, streamable: false },
244
+ trace: { id: messageId, streamable: true },
241
245
  };
242
246
  }
243
247
 
@@ -280,13 +284,13 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
280
284
  log.info("telegram poll loop starting", { gatewayId: opts.id, offset });
281
285
 
282
286
  let stopped = false;
287
+ const stopController = new AbortController();
283
288
  const onAbort = () => {
284
289
  stopped = true;
290
+ stopController.abort();
285
291
  };
286
292
  abortSignal.addEventListener("abort", onAbort, { once: true });
287
- stopCallback = () => {
288
- stopped = true;
289
- };
293
+ stopCallback = onAbort;
290
294
 
291
295
  while (!stopped && !abortSignal.aborted) {
292
296
  try {
@@ -298,6 +302,7 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
298
302
  allowed_updates: ["message"],
299
303
  },
300
304
  (POLL_TIMEOUT_S + 15) * 1000,
305
+ stopController.signal,
301
306
  );
302
307
  markStatus({ lastPollAt: Date.now() });
303
308
  if (!resp.ok) {
@@ -467,3 +472,41 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
467
472
  });
468
473
  }
469
474
 
475
+ function createTimeoutSignal(
476
+ timeoutMs: number,
477
+ parent?: AbortSignal,
478
+ ): { signal: AbortSignal; cleanup: () => void } {
479
+ const controller = new AbortController();
480
+ let settled = false;
481
+
482
+ const abort = (reason?: unknown) => {
483
+ if (settled) return;
484
+ settled = true;
485
+ try {
486
+ controller.abort(reason);
487
+ } catch {
488
+ controller.abort();
489
+ }
490
+ };
491
+
492
+ const timer = setTimeout(() => {
493
+ abort(new DOMException("Timeout", "TimeoutError"));
494
+ }, timeoutMs);
495
+
496
+ const onParentAbort = () => abort(parent?.reason);
497
+ if (parent) {
498
+ if (parent.aborted) {
499
+ onParentAbort();
500
+ } else {
501
+ parent.addEventListener("abort", onParentAbort, { once: true });
502
+ }
503
+ }
504
+
505
+ return {
506
+ signal: controller.signal,
507
+ cleanup: () => {
508
+ clearTimeout(timer);
509
+ if (parent) parent.removeEventListener("abort", onParentAbort);
510
+ },
511
+ };
512
+ }
@@ -803,6 +803,8 @@ export class Dispatcher {
803
803
 
804
804
  const streamable = msg.trace?.streamable === true;
805
805
  const traceId = msg.trace?.id;
806
+ const canType =
807
+ streamable && typeof traceId === "string" && typeof channel.typing === "function";
806
808
  const canStream =
807
809
  streamable && typeof traceId === "string" && typeof channel.streamBlock === "function";
808
810
  const recordBlock = (block: StreamBlock): void => {
@@ -886,7 +888,7 @@ export class Dispatcher {
886
888
  };
887
889
 
888
890
  const fireTypingIfNeeded = (): void => {
889
- if (!canStream || typingFired || typeof channel.typing !== "function") return;
891
+ if (!canType || typingFired) return;
890
892
  typingFired = true;
891
893
  const key = `${msg.accountId}:${msg.conversation.id}`;
892
894
  const now = Date.now();
@@ -9,22 +9,7 @@
9
9
  * wires the production defaults.
10
10
  */
11
11
 
12
- import type {
13
- ControlAck,
14
- GatewayLoginStartParams,
15
- GatewayLoginStartResult,
16
- GatewayLoginStatusParams,
17
- GatewayLoginStatusResult,
18
- GatewayProfileSummary,
19
- GatewayProvider,
20
- ListGatewaysResult,
21
- RemoveGatewayParams,
22
- RemoveGatewayResult,
23
- TestGatewayParams,
24
- TestGatewayResult,
25
- UpsertGatewayParams,
26
- UpsertGatewayResult,
27
- } from "@botcord/protocol-core";
12
+ import type { ControlAck } from "@botcord/protocol-core";
28
13
  import type { Gateway, GatewayChannelConfig } from "./gateway/index.js";
29
14
  import {
30
15
  loadConfig,
@@ -56,6 +41,108 @@ import type { FetchLike } from "./gateway/channels/http-types.js";
56
41
 
57
42
  type AckBody = Omit<ControlAck, "id">;
58
43
 
44
+ type GatewayProvider = "telegram" | "wechat";
45
+
46
+ interface GatewayProfileSummary {
47
+ id: string;
48
+ type: GatewayProvider;
49
+ accountId: string;
50
+ label?: string;
51
+ enabled: boolean;
52
+ baseUrl?: string;
53
+ allowedSenderIds?: string[];
54
+ allowedChatIds?: string[];
55
+ splitAt?: number;
56
+ status?: {
57
+ running: boolean;
58
+ connected?: boolean;
59
+ authorized?: boolean;
60
+ lastPollAt?: number;
61
+ lastInboundAt?: number;
62
+ lastSendAt?: number;
63
+ lastError?: string | null;
64
+ };
65
+ }
66
+
67
+ interface ListGatewaysResult {
68
+ gateways: GatewayProfileSummary[];
69
+ }
70
+
71
+ interface UpsertGatewayParams {
72
+ id: string;
73
+ type: GatewayProvider;
74
+ accountId: string;
75
+ label?: string;
76
+ enabled?: boolean;
77
+ loginId?: string;
78
+ secret?: {
79
+ botToken?: string;
80
+ };
81
+ settings?: {
82
+ baseUrl?: string;
83
+ allowedSenderIds?: string[];
84
+ allowedChatIds?: string[];
85
+ splitAt?: number;
86
+ };
87
+ }
88
+
89
+ interface UpsertGatewayResult {
90
+ id: string;
91
+ type: GatewayProvider;
92
+ accountId: string;
93
+ enabled: boolean;
94
+ tokenPreview?: string;
95
+ status?: GatewayProfileSummary["status"];
96
+ }
97
+
98
+ interface RemoveGatewayParams {
99
+ id: string;
100
+ deleteSecret?: boolean;
101
+ }
102
+
103
+ interface RemoveGatewayResult {
104
+ id: string;
105
+ removed: boolean;
106
+ secretDeleted: boolean;
107
+ }
108
+
109
+ interface TestGatewayParams {
110
+ id: string;
111
+ }
112
+
113
+ interface TestGatewayResult {
114
+ id: string;
115
+ ok: boolean;
116
+ info?: Record<string, unknown>;
117
+ error?: string;
118
+ }
119
+
120
+ interface GatewayLoginStartParams {
121
+ provider: GatewayProvider;
122
+ accountId: string;
123
+ gatewayId?: string;
124
+ baseUrl?: string;
125
+ }
126
+
127
+ interface GatewayLoginStartResult {
128
+ loginId: string;
129
+ qrcode?: string;
130
+ qrcodeUrl?: string;
131
+ expiresAt: number;
132
+ }
133
+
134
+ interface GatewayLoginStatusParams {
135
+ provider: GatewayProvider;
136
+ loginId: string;
137
+ accountId: string;
138
+ }
139
+
140
+ interface GatewayLoginStatusResult {
141
+ status: "pending" | "scanned" | "confirmed" | "expired" | "failed";
142
+ baseUrl?: string;
143
+ tokenPreview?: string;
144
+ }
145
+
59
146
  export type { FetchLike };
60
147
 
61
148
  export interface GatewayControlContext {
package/src/provision.ts CHANGED
@@ -322,10 +322,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
322
322
  return { ok: true, result: snapshot };
323
323
  }
324
324
 
325
- case CONTROL_FRAME_TYPES.LIST_GATEWAYS:
325
+ case "list_gateways":
326
326
  return gatewayControl.handleList();
327
327
 
328
- case CONTROL_FRAME_TYPES.UPSERT_GATEWAY: {
328
+ case "upsert_gateway": {
329
329
  const v = validateGatewayParams(frame.params, {
330
330
  required: ["id", "type", "accountId"],
331
331
  });
@@ -335,7 +335,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
335
335
  );
336
336
  }
337
337
 
338
- case CONTROL_FRAME_TYPES.REMOVE_GATEWAY: {
338
+ case "remove_gateway": {
339
339
  const v = validateGatewayParams(frame.params, { required: ["id"] });
340
340
  if (!v.ok) return v.ack;
341
341
  return gatewayControl.handleRemove(
@@ -343,7 +343,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
343
343
  );
344
344
  }
345
345
 
346
- case CONTROL_FRAME_TYPES.TEST_GATEWAY: {
346
+ case "test_gateway": {
347
347
  const v = validateGatewayParams(frame.params, { required: ["id"] });
348
348
  if (!v.ok) return v.ack;
349
349
  return gatewayControl.handleTest(
@@ -351,7 +351,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
351
351
  );
352
352
  }
353
353
 
354
- case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START: {
354
+ case "gateway_login_start": {
355
355
  const v = validateGatewayParams(frame.params, {
356
356
  required: ["provider", "accountId"],
357
357
  });
@@ -361,7 +361,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
361
361
  );
362
362
  }
363
363
 
364
- case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS: {
364
+ case "gateway_login_status": {
365
365
  const v = validateGatewayParams(frame.params, {
366
366
  required: ["provider", "loginId"],
367
367
  });