@byokit/openclaw 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.1 (2026-10-02)
6
+
7
+ - The README points to the shared account-route vocabulary (D18); `routes()` still returns today's table.
8
+ - FIX: ChatGPT sign-in now interrupts abandoned wizard/paste waits on gateway disconnect and reconfirms a new, usable OAuth profile for the selected member through the reconnected engine. Recovery is bounded and fails closed for unknown, missing, pre-existing or unrelated credentials, cancellation and expiry. A synchronous cleanup error during reconnect no longer suppresses the terminal sign-in view.
9
+
5
10
  ## 0.5.0 (2026-10-01)
6
11
 
7
12
  - Dependency update: pins @byokit/relay 0.5.1.
package/README.md CHANGED
@@ -243,6 +243,8 @@ The kit writes only under the `stateDir` the app passes. It spawns the engine wi
243
243
  (never `process.env`), reads no environment variables except `PATH` to find `npm`, and never bills an API behind the
244
244
  person's back (memory search is never a paid provider).
245
245
 
246
+ Route data follows the shared account-route vocabulary (D18 in [`docs/runtime-kits.md`](../../docs/runtime-kits.md#21-account-routes-d18)): subscription routes are offered by default, and every other billing is used only when named. The full table of the pinned engine's auth choices is planned work; `routes()` returns today's table.
247
+
246
248
  ## Credential sealing and threat model
247
249
 
248
250
  The pinned engine has no supported hook for sealing OAuth profile writes. Its `auth-profiles` loader stores
package/dist/kit.d.ts CHANGED
@@ -52,6 +52,7 @@ export declare class OpenClawKit {
52
52
  private readonly approvalsCtl;
53
53
  private listeners;
54
54
  private off;
55
+ private signInDisconnects;
55
56
  private starting?;
56
57
  private stopping;
57
58
  private failures;
package/dist/kit.js CHANGED
@@ -427,6 +427,7 @@ export class OpenClawKit {
427
427
  approvalsCtl;
428
428
  listeners = new Set();
429
429
  off = [];
430
+ signInDisconnects = new Set();
430
431
  starting;
431
432
  stopping = false;
432
433
  failures = 0;
@@ -535,6 +536,8 @@ export class OpenClawKit {
535
536
  const engineFailed = _why === 'engine exited' && this.current.phase === 'failed' && this.current.why === 'exited';
536
537
  if (this.stopping || (this.current.phase !== 'ready' && !engineFailed))
537
538
  return;
539
+ for (const fn of this.signInDisconnects)
540
+ fn();
538
541
  void this.disconnect();
539
542
  if (this.o.spawnEngine === false) {
540
543
  this.setState({ phase: 'failed', why: 'handshake' });
@@ -590,7 +593,8 @@ export class OpenClawKit {
590
593
  // sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
591
594
  signInCtx() {
592
595
  return { request: (method, params, o) => this.request()(method, params, o),
593
- ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455 };
596
+ ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
597
+ onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
594
598
  }
595
599
  routes() {
596
600
  return routeTable();
package/dist/signin.d.ts CHANGED
@@ -5,6 +5,7 @@ export type SignInCtx = {
5
5
  agentId: string;
6
6
  }>;
7
7
  callbackPort: number;
8
+ onDisconnect?(fn: () => void): () => void;
8
9
  };
9
10
  /**
10
11
  * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
package/dist/signin.js CHANGED
@@ -12,6 +12,7 @@ const START_MS = 60_000;
12
12
  const PULL_MS = 120_000;
13
13
  const STATUS_MS = 20_000;
14
14
  const CANCEL_MS = 10_000;
15
+ const RECONFIRM_MS = 60_000;
15
16
  const TURNS = 200;
16
17
  const PASTE_MS = 15 * 60_000;
17
18
  const cut = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 200);
@@ -25,6 +26,7 @@ export function signIn(ctx, member, o, on) {
25
26
  const via = o.via ?? 'browser';
26
27
  const owner = new AbortController();
27
28
  const { signal } = owner;
29
+ const driveOwner = new AbortController();
28
30
  const wantsCallback = via === 'browser' && o.authChoice === 'openai';
29
31
  let server;
30
32
  let sessionId = '';
@@ -33,6 +35,24 @@ export function signIn(ctx, member, o, on) {
33
35
  let settleStart;
34
36
  const startingSettled = new Promise((resolve) => { settleStart = resolve; });
35
37
  let over = false;
38
+ let recovering = false;
39
+ let offDisconnect;
40
+ let agentId;
41
+ let before;
42
+ // Only ChatGPT's OAuth routes are qualified for auth-reload recovery. No API-key fallback.
43
+ const reconfirm = o.authChoice === 'openai' || o.authChoice === 'openai-device-code';
44
+ // Pin models-auth-status: profiles carry {profileId, type, status, expiry: {at, remainingMs, label}}.
45
+ // Older summaries are not credential proof; a refreshed pre-existing id cannot stand in for this login.
46
+ const profiles = (status, usable = true) => {
47
+ const auth = status;
48
+ if (auth?.unavailable || !Array.isArray(auth?.providers) || auth.providers.some((row) => !row || typeof row !== 'object' || typeof row.provider !== 'string' || !Array.isArray(row.profiles)
49
+ || row.profiles.some((p) => !p || typeof p.profileId !== 'string' || typeof p.type !== 'string')))
50
+ return undefined;
51
+ return new Set(auth.providers.filter((row) => row.provider === 'openai').flatMap((row) => (row.profiles ?? []).filter((p) => p.type === 'oauth' && typeof p.profileId === 'string'
52
+ && (!usable || ((p.status === 'ok' || p.status === 'expiring') && Number.isFinite(p.expiry?.at)
53
+ && p.expiry.at > Date.now())))
54
+ .map((p) => p.profileId)));
55
+ };
36
56
  let approval = false;
37
57
  let sawDeviceCode = false;
38
58
  let codeExpired = false;
@@ -49,7 +69,10 @@ export function signIn(ctx, member, o, on) {
49
69
  let pasteIn;
50
70
  let returned;
51
71
  const done = new Promise((resolve) => { settle = resolve; });
52
- const say = (v) => on({ state: 'waiting', via, ...v });
72
+ const say = (v) => {
73
+ if (!over && !recovering && !signal.aborted)
74
+ on({ state: 'waiting', via, ...v });
75
+ };
53
76
  const closeCallback = async () => {
54
77
  const held = server;
55
78
  server = undefined;
@@ -61,6 +84,7 @@ export function signIn(ctx, member, o, on) {
61
84
  return;
62
85
  over = true;
63
86
  stopApproval();
87
+ offDisconnect?.();
64
88
  o.signal?.removeEventListener('abort', cancel);
65
89
  owner.abort();
66
90
  returned = undefined;
@@ -76,7 +100,7 @@ export function signIn(ctx, member, o, on) {
76
100
  // and leaves the person locked out of signing in. Waiting for that settlement is what frees the admission.
77
101
  await startingSettled;
78
102
  if (sessionId)
79
- await ctx.request('wizard.cancel', { sessionId }, { timeoutMs: CANCEL_MS }).catch(() => { });
103
+ await Promise.resolve().then(() => ctx.request('wizard.cancel', { sessionId }, { timeoutMs: CANCEL_MS })).catch(() => { });
80
104
  })();
81
105
  return releasing;
82
106
  };
@@ -130,12 +154,18 @@ export function signIn(ctx, member, o, on) {
130
154
  });
131
155
  };
132
156
  const drive = async () => {
157
+ const signal = AbortSignal.any([owner.signal, driveOwner.signal]);
133
158
  if (wantsCallback) {
134
159
  server = await holdCallback().catch(() => undefined);
135
160
  if (!server)
136
161
  return { state: 'failed', via, why: 'busy' };
137
162
  }
138
- const { agentId } = await ctx.ensure(member);
163
+ ({ agentId } = await ctx.ensure(member));
164
+ if (reconfirm && ctx.onDisconnect) {
165
+ before = profiles(await authStatus(ctx.request, agentId).catch(() => undefined), false);
166
+ if (signal.aborted)
167
+ return cancelled();
168
+ }
139
169
  // Native login is completed by the person in unmodified Claude Code. The pin does not expose this
140
170
  // choice via auth.start; activation verifies the CLI route and persists it only after a live test succeeds.
141
171
  if (o.authChoice === 'anthropic-cli' || o.authChoice === 'apiKey') {
@@ -292,10 +322,61 @@ export function signIn(ctx, member, o, on) {
292
322
  return expired();
293
323
  return error ? { state: 'failed', via, why: 'failed', error: cut(error) } : { state: 'done', via };
294
324
  };
325
+ const disconnected = new Promise((resolve) => {
326
+ if (reconfirm)
327
+ offDisconnect = ctx.onDisconnect?.(() => {
328
+ recovering = true;
329
+ resolve(undefined); // win the race before the old request's abort/close rejection propagates
330
+ driveOwner.abort();
331
+ });
332
+ });
333
+ const recover = async () => {
334
+ const failed = { state: 'failed', via, why: 'failed', error: 'Sign-in could not be confirmed after the gateway restarted. Try again.' };
335
+ if (!agentId || !before)
336
+ return failed;
337
+ const recovery = new AbortController();
338
+ const recoverySignal = AbortSignal.any([signal, recovery.signal]);
339
+ let timer;
340
+ let abort;
341
+ const end = new Promise((resolve) => {
342
+ abort = () => resolve(codeExpired ? expired() : cancelled());
343
+ signal.addEventListener('abort', abort, { once: true });
344
+ timer = setTimeout(() => resolve(failed), RECONFIRM_MS);
345
+ if (signal.aborted)
346
+ abort();
347
+ });
348
+ const readback = async () => {
349
+ while (!recoverySignal.aborted && !over) {
350
+ const status = await Promise.resolve().then(() => ctx.request('models.authStatus', { agentId, refresh: true }, { timeoutMs: STATUS_MS, signal: recoverySignal })).catch(() => undefined);
351
+ const after = profiles(status);
352
+ if (!recoverySignal.aborted && after && [...after].some((p) => !before.has(p)))
353
+ return { state: 'done', via };
354
+ await new Promise((resolve) => {
355
+ const stop = () => { clearTimeout(wait); recoverySignal.removeEventListener('abort', stop); resolve(); };
356
+ const wait = setTimeout(stop, 1_000);
357
+ recoverySignal.addEventListener('abort', stop, { once: true });
358
+ if (recoverySignal.aborted)
359
+ stop();
360
+ });
361
+ }
362
+ return codeExpired ? expired() : cancelled();
363
+ };
364
+ try {
365
+ return await Promise.race([readback(), end]);
366
+ }
367
+ finally {
368
+ recovery.abort();
369
+ clearTimeout(timer);
370
+ signal.removeEventListener('abort', abort);
371
+ }
372
+ };
295
373
  void (async () => {
296
374
  let view;
297
375
  try {
298
- view = await drive();
376
+ const driven = await Promise.race([drive(), disconnected]);
377
+ view = driven ?? await recover();
378
+ if (signal.aborted)
379
+ view = codeExpired ? expired() : cancelled();
299
380
  }
300
381
  catch (error) {
301
382
  if (signal.aborted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",