@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 +5 -0
- package/README.md +2 -0
- package/dist/kit.d.ts +1 -0
- package/dist/kit.js +5 -1
- package/dist/signin.d.ts +1 -0
- package/dist/signin.js +85 -4
- package/package.json +1 -1
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
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
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|