@modelprofile.com/authswitch 10.0.0 → 10.1.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/accounts.d.ts +0 -17
- package/dist_ts/accounts.js +2 -82
- package/dist_ts/classes.authoritycli.js +3 -2
- package/dist_ts/classes.claudeauthority.d.ts +5 -0
- package/dist_ts/classes.claudeauthority.js +47 -4
- package/dist_ts/classes.claudecodelocks.d.ts +6 -0
- package/dist_ts/classes.claudecodelocks.js +30 -2
- package/dist_ts/classes.claudenative.d.ts +37 -2
- package/dist_ts/classes.claudenative.js +142 -13
- package/dist_ts/classes.cli.js +3 -2
- package/dist_ts/classes.codexpreuse.js +1 -1
- package/dist_ts/classes.limits.js +3 -2
- package/dist_ts/classes.listrenderer.js +3 -2
- package/dist_ts/classes.tui.js +3 -2
- package/dist_ts/classes.watch.js +3 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/preuse-text.d.ts +18 -0
- package/dist_ts/preuse-text.js +28 -0
- package/dist_ts/preuse.d.ts +9 -36
- package/dist_ts/preuse.js +3 -27
- package/dist_ts/timetext.d.ts +23 -0
- package/dist_ts/timetext.js +88 -0
- package/dist_ts/watchpolicy.js +3 -2
- package/package.json +5 -1
- package/readme.md +38 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/accounts.ts +1 -69
- package/ts/classes.authoritycli.ts +2 -1
- package/ts/classes.claudeauthority.ts +37 -3
- package/ts/classes.claudecodelocks.ts +31 -1
- package/ts/classes.claudenative.ts +161 -14
- package/ts/classes.cli.ts +2 -1
- package/ts/classes.codexpreuse.ts +3 -3
- package/ts/classes.limits.ts +2 -1
- package/ts/classes.listrenderer.ts +2 -1
- package/ts/classes.tui.ts +2 -1
- package/ts/classes.watch.ts +2 -1
- package/ts/index.ts +1 -0
- package/ts/preuse-text.ts +40 -0
- package/ts/preuse.ts +9 -55
- package/ts/timetext.ts +76 -0
- package/ts/watchpolicy.ts +2 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human time text: countdowns to a deadline and timestamps in local calendar time.
|
|
3
|
+
*
|
|
4
|
+
* It imports nothing, so the browser-safe `./preuse-text` entry point can render a reset exactly as the
|
|
5
|
+
* command line does without pulling a Node built-in into a bundle.
|
|
6
|
+
*/
|
|
7
|
+
/** A countdown is a snapshot of the provider's deadline, not proof a quota has reset. */
|
|
8
|
+
export const until = (timestampArg, nowArg) => {
|
|
9
|
+
if (timestampArg === null)
|
|
10
|
+
return 'Not scheduled';
|
|
11
|
+
const remaining = Date.parse(timestampArg) - nowArg;
|
|
12
|
+
if (!Number.isFinite(remaining))
|
|
13
|
+
return 'Unavailable';
|
|
14
|
+
if (remaining <= 0)
|
|
15
|
+
return 'due';
|
|
16
|
+
const minutes = Math.floor(remaining / 60000);
|
|
17
|
+
if (!minutes)
|
|
18
|
+
return '<1min';
|
|
19
|
+
const days = Math.floor(minutes / 1440);
|
|
20
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
21
|
+
return [days ? `${days}d` : '', hours ? `${hours}h` : '', minutes % 60 ? `${minutes % 60}min` : ''].filter(Boolean).join(' ');
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A two-unit countdown for condensed tables: `2h 13m`, `3d 4h`, `45m`.
|
|
25
|
+
*
|
|
26
|
+
* `until` stays the long form used by the list view and the TUI. This one trades the third unit for
|
|
27
|
+
* a column that never wraps, and it never invents a deadline: an unknown or unparseable timestamp is
|
|
28
|
+
* reported as such rather than as zero time remaining.
|
|
29
|
+
*/
|
|
30
|
+
const twoUnits = (millisecondsArg) => {
|
|
31
|
+
const minutes = Math.floor(millisecondsArg / 60000);
|
|
32
|
+
const days = Math.floor(minutes / 1440);
|
|
33
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
34
|
+
if (days)
|
|
35
|
+
return hours ? `${days}d ${hours}h` : `${days}d`;
|
|
36
|
+
if (hours)
|
|
37
|
+
return minutes % 60 ? `${hours}h ${minutes % 60}m` : `${hours}h`;
|
|
38
|
+
return `${minutes}m`;
|
|
39
|
+
};
|
|
40
|
+
export const compactUntil = (timestampArg, nowArg, unavailableArg = 'n/a') => {
|
|
41
|
+
if (timestampArg === null)
|
|
42
|
+
return 'not scheduled';
|
|
43
|
+
const remaining = Date.parse(timestampArg) - nowArg;
|
|
44
|
+
if (!Number.isFinite(remaining))
|
|
45
|
+
return unavailableArg;
|
|
46
|
+
if (remaining <= 0)
|
|
47
|
+
return 'due';
|
|
48
|
+
return remaining < 60000 ? '<1m' : twoUnits(remaining);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The same two-unit form for a past timestamp, for "saved 3d 4h ago".
|
|
52
|
+
*
|
|
53
|
+
* The result is always a bare duration or the caller's unavailable marker, never a phrase: callers
|
|
54
|
+
* suffix it ("... ago"), and a phrase would read as "just now ago". A span under a minute is `<1m`,
|
|
55
|
+
* exactly as `compactUntil` reports one.
|
|
56
|
+
*/
|
|
57
|
+
export const compactSince = (timestampArg, nowArg, unavailableArg = 'n/a') => {
|
|
58
|
+
if (timestampArg === null)
|
|
59
|
+
return unavailableArg;
|
|
60
|
+
const elapsed = nowArg - Date.parse(timestampArg);
|
|
61
|
+
if (!Number.isFinite(elapsed))
|
|
62
|
+
return unavailableArg;
|
|
63
|
+
return elapsed < 60000 ? '<1m' : twoUnits(elapsed);
|
|
64
|
+
};
|
|
65
|
+
const twoDigits = (valueArg) => String(valueArg).padStart(2, '0');
|
|
66
|
+
/**
|
|
67
|
+
* A timestamp in local calendar time: `2026-10-08`, or `2026-10-08 23:14` with the time. Human output never prints
|
|
68
|
+
* a raw ISO timestamp; an unknown or unparseable one reads as the caller's unavailable marker.
|
|
69
|
+
*/
|
|
70
|
+
const localTimestamp = (timestampArg, withTimeArg, unavailableArg) => {
|
|
71
|
+
const time = timestampArg === null ? Number.NaN : Date.parse(timestampArg);
|
|
72
|
+
if (!Number.isFinite(time))
|
|
73
|
+
return unavailableArg;
|
|
74
|
+
const date = new Date(time);
|
|
75
|
+
const day = `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
|
|
76
|
+
return withTimeArg ? `${day} ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}` : day;
|
|
77
|
+
};
|
|
78
|
+
export const localDate = (timestampArg, unavailableArg = 'n/a') => localTimestamp(timestampArg, false, unavailableArg);
|
|
79
|
+
export const localDateTime = (timestampArg, unavailableArg = 'n/a') => localTimestamp(timestampArg, true, unavailableArg);
|
|
80
|
+
/** A moment's local time of day with seconds, `14:02:07`, for a log line whose date is implied. */
|
|
81
|
+
export const localTime = (timeArg) => {
|
|
82
|
+
const date = new Date(timeArg);
|
|
83
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()].map(twoDigits).join(':');
|
|
84
|
+
};
|
|
85
|
+
const ISO_TIMESTAMP = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})/g;
|
|
86
|
+
/** Provider text with every ISO 8601 timestamp in it rewritten as local date and time. */
|
|
87
|
+
export const localiseTimestamps = (textArg) => textArg.replace(ISO_TIMESTAMP, match => localDateTime(match, match));
|
|
88
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGltZXRleHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90aW1ldGV4dC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7R0FLRztBQUVILHlGQUF5RjtBQUN6RixNQUFNLENBQUMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxZQUEyQixFQUFFLE1BQWMsRUFBVSxFQUFFO0lBQzNFLElBQUksWUFBWSxLQUFLLElBQUk7UUFBRSxPQUFPLGVBQWUsQ0FBQztJQUNsRCxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxHQUFHLE1BQU0sQ0FBQztJQUNwRCxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUM7UUFBRSxPQUFPLGFBQWEsQ0FBQztJQUN0RCxJQUFJLFNBQVMsSUFBSSxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDakMsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLENBQUM7SUFDOUMsSUFBSSxDQUFDLE9BQU87UUFBRSxPQUFPLE9BQU8sQ0FBQztJQUM3QixNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUMsQ0FBQztJQUN4QyxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sR0FBRyxJQUFJLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDOUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLE9BQU8sR0FBRyxFQUFFLENBQUMsQ0FBQyxDQUFDLEdBQUcsT0FBTyxHQUFHLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsT0FB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
|
package/dist_ts/watchpolicy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { usagePercentText } from './accounts.js';
|
|
2
|
+
import { compactUntil } from './timetext.js';
|
|
2
3
|
import { plainText } from './formatting.js';
|
|
3
4
|
/**
|
|
4
5
|
* How far below the threshold a switch target must be on every limit, so that a switch never lands on an account that
|
|
@@ -79,4 +80,4 @@ export const decideWatchAction = (inputArg) => {
|
|
|
79
80
|
}
|
|
80
81
|
return stay(true, `every limit below ${threshold}%`);
|
|
81
82
|
};
|
|
82
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
83
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hwb2xpY3kuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy93YXRjaHBvbGljeS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDakQsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUM3QyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUE4QjVDOzs7R0FHRztBQUNILE1BQU0sZ0JBQWdCLEdBQUcsRUFBRSxDQUFDO0FBRTVCLE1BQU0sY0FBYyxHQUFHLENBQUMsVUFBeUIsRUFBeUIsRUFBRSxDQUFDLENBQUMsVUFBVSxDQUFDLE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDO0FBQ3JKLE1BQU0sY0FBYyxHQUFHLENBQUMsVUFBeUIsRUFBeUIsRUFBRSxDQUFDLENBQUMsVUFBVSxDQUFDLE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDO0FBRXJKLDZIQUE2SDtBQUM3SCxNQUFNLE9BQU8sR0FBRyxDQUFDLFVBQTBDLEVBQUUsWUFBb0IsRUFBbUMsRUFBRSxDQUNwSCxVQUFVLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFdBQVcsSUFBSSxZQUFZLENBQUM7S0FDNUQsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUMsV0FBVyxJQUFJLEtBQUssQ0FBQyxlQUFlLEdBQUcsSUFBSSxDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRXBILE1BQU0sU0FBUyxH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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/authswitch",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Manage Codex, OpenCode and Claude Code accounts with guided switching and live usage status",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist_ts/mutation.d.ts",
|
|
15
15
|
"import": "./dist_ts/mutation.js"
|
|
16
16
|
},
|
|
17
|
+
"./preuse-text": {
|
|
18
|
+
"types": "./dist_ts/preuse-text.d.ts",
|
|
19
|
+
"import": "./dist_ts/preuse-text.js"
|
|
20
|
+
},
|
|
17
21
|
"./authority-contract": {
|
|
18
22
|
"types": "./dist_ts/authority-contract.d.ts",
|
|
19
23
|
"import": "./dist_ts/authority-contract.js"
|
package/readme.md
CHANGED
|
@@ -291,6 +291,26 @@ reason is `outcome_unknown` with problem `request_uncertain` and carries its cau
|
|
|
291
291
|
(`{ kind, status, providerCode }`, for example `{ "kind": "http", "status": 503, "providerCode": null }`),
|
|
292
292
|
never the provider's text. Human output prints both as sentences.
|
|
293
293
|
|
|
294
|
+
A host that shows receipts in a browser words them the same way through
|
|
295
|
+
`@modelprofile.com/authswitch/preuse-text`, the browser-safe entry point for those sentences:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { describePreuseFailure, describePreuseLimit, localDateTime } from '@modelprofile.com/authswitch/preuse-text';
|
|
299
|
+
import type { IAuthSwitchPreuseOperation } from '@modelprofile.com/authswitch/authority-contract';
|
|
300
|
+
|
|
301
|
+
const lines = (operation: IAuthSwitchPreuseOperation): string[] => [
|
|
302
|
+
...(operation.limit ? [describePreuseLimit(operation.limit, Date.now())] : []),
|
|
303
|
+
...(operation.failure ? [`Cause: ${describePreuseFailure(operation.failure)}`] : []),
|
|
304
|
+
];
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`describePreuseLimit(limit, now)` names the window and the limit, and a stated reset as a countdown from
|
|
308
|
+
`now` with the local time (`Weekly usage limit reached, resets in 16h 46min (2026-09-27 05:12)`, or `due
|
|
309
|
+
to reset now` once it has passed); `describePreuseFailure(failure)` names the cause as the tail of a
|
|
310
|
+
sentence (`the provider answered HTTP 503`). `localDateTime(timestamp)` is the local time both use,
|
|
311
|
+
`2026-09-27 05:12`. The module loads nothing but that time text, which imports nothing either; the package
|
|
312
|
+
root exports the same three functions from the same source.
|
|
313
|
+
|
|
294
314
|
For `account add`, `account reauth` and `account operation resume`, `--json` writes JSON Lines:
|
|
295
315
|
each line is one complete operation receipt, and unchanged revisions are omitted. `account
|
|
296
316
|
operation get --json` and `account operation cancel --json` each write one JSON object followed by
|
|
@@ -332,10 +352,21 @@ been adopted. Until a verified `claude_native` import has adopted one, every Cla
|
|
|
332
352
|
`CLAUDE_CONFIG_DIR` that is not an absolute path is refused when the daemon starts, because the same
|
|
333
353
|
variable also decides which file the import would read; it is never resolved against a working directory.
|
|
334
354
|
|
|
335
|
-
A switch is only attempted when the host can prove Claude Code uses that native login:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
355
|
+
A switch is only attempted when the host can prove Claude Code uses that native login: Claude Code
|
|
356
|
+
2.1.273 or newer installed, a subscriber login in the store, no credential override in the environment
|
|
357
|
+
(including the remote well-known token files), no managed or local settings selecting another auth source,
|
|
358
|
+
and every Claude Code session running in that home on a verified release -- one whose lock table, live
|
|
359
|
+
credential pickup and refresh compare-and-swap were read from its bundle, currently 2.1.273 and 2.1.282.
|
|
360
|
+
An installed release above 2.1.273 that is not verified is proven by Claude Code's own answer instead:
|
|
361
|
+
`claude auth status --json` must report the claude.ai login of the outgoing account -- its organization and
|
|
362
|
+
email, the only account identifiers it names -- read from this home and with no API key, before the write,
|
|
363
|
+
and of the incoming account after it. That path runs only while
|
|
364
|
+
no Claude Code session runs in the home. If Claude Code does not report the incoming login after the
|
|
365
|
+
write, or the proof fails for any other reason, the exact outgoing bytes are written back while the locks
|
|
366
|
+
are provably still held and the handoff is aborted with `problem: unsupported_effective_auth`; when they
|
|
367
|
+
cannot be, the home is quarantined. A write whose proof never ran is proven by the reconcile that finds it
|
|
368
|
+
before it can commit. Anything else refuses and leaves
|
|
369
|
+
both logins where they were. A move that reaches the home and
|
|
339
370
|
then cannot complete that proof is quarantined with `problem: unsupported_effective_auth`, and the
|
|
340
371
|
handoff's optional `proofFailure` names which condition failed -- `unsupported_release`, `subscription`,
|
|
341
372
|
`override`, `settings`, `profile`, `profile_unreadable` or `running_session` -- so an owner is told what
|
|
@@ -649,7 +680,7 @@ copy's own tokens, and nothing else. `preuse` remains a Codex capability.
|
|
|
649
680
|
|
|
650
681
|
**Claude Code picks a switch up live.** A running session re-reads its credential
|
|
651
682
|
file whenever the file changes, before its next request, and reloads `.claude.json`
|
|
652
|
-
when that changes (verified against Claude Code 2.1.273). So `use`, `stash` and a
|
|
683
|
+
when that changes (verified against Claude Code 2.1.273 and 2.1.282). So `use`, `stash` and a
|
|
653
684
|
backend activation never ask for Claude Code to be stopped and never suggest a
|
|
654
685
|
restart; a switch reports `Running Claude Code sessions use it from their next
|
|
655
686
|
request.` What authswitch must not do is write in the middle of that session's own
|
|
@@ -773,8 +804,8 @@ such as AGL all read this one rule; `remove` and a save that keeps the login
|
|
|
773
804
|
active leave the loaded login in place.
|
|
774
805
|
|
|
775
806
|
That rule, `assertAuthSwitchMutation` and the `TAuthSwitchMutation` command are also
|
|
776
|
-
published on their own at `@modelprofile.com/authswitch/mutation`, which
|
|
777
|
-
|
|
807
|
+
published on their own at `@modelprofile.com/authswitch/mutation`, which a browser can
|
|
808
|
+
bundle, as it can the preuse sentences at `./preuse-text`:
|
|
778
809
|
|
|
779
810
|
```ts
|
|
780
811
|
import { authSwitchMutationReplacesLogin, type TAuthSwitchMutation } from '@modelprofile.com/authswitch/mutation';
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/accounts.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IAccountType, IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, IHarnessState, IHarnessStatusOptions, IHarnessUsageWindow } from './interfaces.harness.js';
|
|
2
2
|
import { plainText } from './formatting.js';
|
|
3
|
+
import { compactUntil, localDateTime, localiseTimestamps, until } from './timetext.js';
|
|
3
4
|
|
|
4
5
|
export interface IAccountRow {
|
|
5
6
|
harness: IAuthHarness;
|
|
@@ -23,75 +24,6 @@ const WINDOW_PERIODS: ReadonlyMap<number, string> = new Map([[18000, 'five-hour'
|
|
|
23
24
|
* The narrow table cells stay on the compact form throughout (`5h 37% · 2h 13m`); a column is not a label.
|
|
24
25
|
*/
|
|
25
26
|
export const usageWindowPeriod = (secondsArg: number): string => WINDOW_PERIODS.get(secondsArg) ?? duration(secondsArg);
|
|
26
|
-
/** A countdown is a snapshot of the provider's deadline, not proof a quota has reset. */
|
|
27
|
-
export const until = (timestampArg: string | null, nowArg: number): string => {
|
|
28
|
-
if (timestampArg === null) return 'Not scheduled';
|
|
29
|
-
const remaining = Date.parse(timestampArg) - nowArg;
|
|
30
|
-
if (!Number.isFinite(remaining)) return 'Unavailable';
|
|
31
|
-
if (remaining <= 0) return 'due';
|
|
32
|
-
const minutes = Math.floor(remaining / 60000);
|
|
33
|
-
if (!minutes) return '<1min';
|
|
34
|
-
const days = Math.floor(minutes / 1440);
|
|
35
|
-
const hours = Math.floor(minutes % 1440 / 60);
|
|
36
|
-
return [days ? `${days}d` : '', hours ? `${hours}h` : '', minutes % 60 ? `${minutes % 60}min` : ''].filter(Boolean).join(' ');
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* A two-unit countdown for condensed tables: `2h 13m`, `3d 4h`, `45m`.
|
|
40
|
-
*
|
|
41
|
-
* `until` stays the long form used by the list view and the TUI. This one trades the third unit for
|
|
42
|
-
* a column that never wraps, and it never invents a deadline: an unknown or unparseable timestamp is
|
|
43
|
-
* reported as such rather than as zero time remaining.
|
|
44
|
-
*/
|
|
45
|
-
const twoUnits = (millisecondsArg: number): string => {
|
|
46
|
-
const minutes = Math.floor(millisecondsArg / 60000);
|
|
47
|
-
const days = Math.floor(minutes / 1440);
|
|
48
|
-
const hours = Math.floor(minutes % 1440 / 60);
|
|
49
|
-
if (days) return hours ? `${days}d ${hours}h` : `${days}d`;
|
|
50
|
-
if (hours) return minutes % 60 ? `${hours}h ${minutes % 60}m` : `${hours}h`;
|
|
51
|
-
return `${minutes}m`;
|
|
52
|
-
};
|
|
53
|
-
export const compactUntil = (timestampArg: string | null, nowArg: number, unavailableArg = 'n/a'): string => {
|
|
54
|
-
if (timestampArg === null) return 'not scheduled';
|
|
55
|
-
const remaining = Date.parse(timestampArg) - nowArg;
|
|
56
|
-
if (!Number.isFinite(remaining)) return unavailableArg;
|
|
57
|
-
if (remaining <= 0) return 'due';
|
|
58
|
-
return remaining < 60000 ? '<1m' : twoUnits(remaining);
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* The same two-unit form for a past timestamp, for "saved 3d 4h ago".
|
|
62
|
-
*
|
|
63
|
-
* The result is always a bare duration or the caller's unavailable marker, never a phrase: callers
|
|
64
|
-
* suffix it ("... ago"), and a phrase would read as "just now ago". A span under a minute is `<1m`,
|
|
65
|
-
* exactly as `compactUntil` reports one.
|
|
66
|
-
*/
|
|
67
|
-
export const compactSince = (timestampArg: string | null, nowArg: number, unavailableArg = 'n/a'): string => {
|
|
68
|
-
if (timestampArg === null) return unavailableArg;
|
|
69
|
-
const elapsed = nowArg - Date.parse(timestampArg);
|
|
70
|
-
if (!Number.isFinite(elapsed)) return unavailableArg;
|
|
71
|
-
return elapsed < 60000 ? '<1m' : twoUnits(elapsed);
|
|
72
|
-
};
|
|
73
|
-
const twoDigits = (valueArg: number): string => String(valueArg).padStart(2, '0');
|
|
74
|
-
/**
|
|
75
|
-
* A timestamp in local calendar time: `2026-10-08`, or `2026-10-08 23:14` with the time. Human output never prints
|
|
76
|
-
* a raw ISO timestamp; an unknown or unparseable one reads as the caller's unavailable marker.
|
|
77
|
-
*/
|
|
78
|
-
const localTimestamp = (timestampArg: string | null, withTimeArg: boolean, unavailableArg: string): string => {
|
|
79
|
-
const time = timestampArg === null ? Number.NaN : Date.parse(timestampArg);
|
|
80
|
-
if (!Number.isFinite(time)) return unavailableArg;
|
|
81
|
-
const date = new Date(time);
|
|
82
|
-
const day = `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
|
|
83
|
-
return withTimeArg ? `${day} ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}` : day;
|
|
84
|
-
};
|
|
85
|
-
export const localDate = (timestampArg: string | null, unavailableArg = 'n/a'): string => localTimestamp(timestampArg, false, unavailableArg);
|
|
86
|
-
export const localDateTime = (timestampArg: string | null, unavailableArg = 'n/a'): string => localTimestamp(timestampArg, true, unavailableArg);
|
|
87
|
-
/** A moment's local time of day with seconds, `14:02:07`, for a log line whose date is implied. */
|
|
88
|
-
export const localTime = (timeArg: number): string => {
|
|
89
|
-
const date = new Date(timeArg);
|
|
90
|
-
return [date.getHours(), date.getMinutes(), date.getSeconds()].map(twoDigits).join(':');
|
|
91
|
-
};
|
|
92
|
-
const ISO_TIMESTAMP = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})/g;
|
|
93
|
-
/** Provider text with every ISO 8601 timestamp in it rewritten as local date and time. */
|
|
94
|
-
export const localiseTimestamps = (textArg: string): string => textArg.replace(ISO_TIMESTAMP, match => localDateTime(match, match));
|
|
95
27
|
|
|
96
28
|
/**
|
|
97
29
|
* One sentence for a credential file that no longer holds the account the last switch wrote.
|
|
@@ -6,7 +6,8 @@ import type { IAuthSwitchImportInventory, IAuthSwitchImportResult, IAuthSwitchIm
|
|
|
6
6
|
IReq_AuthSwitchImportStatus, TAuthSwitchImportAction } from './authority-import-contract.js';
|
|
7
7
|
import { AuthSwitchClient } from './classes.authorityclient.js';
|
|
8
8
|
import { resolveAuthSwitchAuthorityPaths } from './classes.authorityservice.js';
|
|
9
|
-
import { defaultPreusePrompt,
|
|
9
|
+
import { defaultPreusePrompt, validatePreuseOptions } from './preuse.js';
|
|
10
|
+
import { describePreuseFailure, describePreuseLimit } from './preuse-text.js';
|
|
10
11
|
import { plainText } from './formatting.js';
|
|
11
12
|
import * as plugins from './plugins.js';
|
|
12
13
|
|
|
@@ -418,7 +418,13 @@ export class ClaudeNativeAuthority {
|
|
|
418
418
|
return publicHandoff(result.handoff);
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Settles a handoff from what its native home holds. A pair that is exactly the prepared target commits only
|
|
423
|
+
* once Claude Code is proven to select it: by the switch that just wrote it (`targetProven`), or here, under
|
|
424
|
+
* this lease -- which is what a restarted daemon relies on for a write whose proof it never ran.
|
|
425
|
+
*/
|
|
426
|
+
private async classify(operationId: string, session: IClaudeNativeLockedSession,
|
|
427
|
+
options: { targetProven?: boolean } = {}): Promise<IAuthSwitchClaudeNativeHandoff> {
|
|
422
428
|
const context = await this.database.readClaudeHandoffContext(operationId);
|
|
423
429
|
if (!context) throw new Error('Claude handoff operation is missing.');
|
|
424
430
|
const handoff = context.handoff;
|
|
@@ -441,6 +447,14 @@ export class ClaudeNativeAuthority {
|
|
|
441
447
|
if (handoff.after && samePair(observed, handoff.after)
|
|
442
448
|
&& sameIdentity(observed.identity, handoff.targetIdentity)
|
|
443
449
|
&& accessDigest === handoff.targetAccessDigest) {
|
|
450
|
+
if (!options.targetProven) {
|
|
451
|
+
try { await session.proveEffectiveLogin(await this.unsealLogin(context.incomingAccount, context.incoming)); }
|
|
452
|
+
catch (error) {
|
|
453
|
+
// The original bytes are not journaled, so an unproven write cannot be undone here; it is held apart.
|
|
454
|
+
return this.settle(operationId, 'quarantined', observed.runningEffectiveAuth, 'unsupported_effective_auth',
|
|
455
|
+
error instanceof ClaudeNativeEffectiveAuthUnsupportedError ? error.reason : null);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
444
458
|
return this.settle(operationId, 'committed', observed.runningEffectiveAuth);
|
|
445
459
|
}
|
|
446
460
|
return this.settle(operationId, 'quarantined', observed.runningEffectiveAuth, 'foreign_or_torn');
|
|
@@ -504,6 +518,14 @@ export class ClaudeNativeAuthority {
|
|
|
504
518
|
if (!context || !sameIdentity(before.identity, context.handoff.sourceIdentity) || !before.login) {
|
|
505
519
|
return this.classify(operationId, session);
|
|
506
520
|
}
|
|
521
|
+
// Claude Code must be running on the outgoing login before anything is written, or nothing here says
|
|
522
|
+
// what the handoff replaces; nothing is written yet, so the reservation is simply undone.
|
|
523
|
+
try { await session.proveEffectiveLogin(before.login); }
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (!(error instanceof ClaudeNativeEffectiveAuthUnsupportedError)) throw error;
|
|
526
|
+
return this.settle(operationId, 'aborted', before.runningEffectiveAuth, 'unsupported_effective_auth',
|
|
527
|
+
error.reason);
|
|
528
|
+
}
|
|
507
529
|
let outgoingProof: Awaited<ReturnType<IClaudeNativeLockedSession['verifyProfileIdentity']>>;
|
|
508
530
|
try { outgoingProof = await session.verifyProfileIdentity(before, context.handoff.sourceIdentity); }
|
|
509
531
|
catch {
|
|
@@ -518,8 +540,20 @@ export class ClaudeNativeAuthority {
|
|
|
518
540
|
await this.prepare(operationId, loginId, prepared.plan.before, prepared.plan.after,
|
|
519
541
|
outgoingProof.accessTokenDigest, prepared.plan.profile.accessTokenDigest,
|
|
520
542
|
sealedOutgoing, before.runningEffectiveAuth);
|
|
521
|
-
session.writePreparedIfCurrent(prepared.plan);
|
|
522
|
-
return this.classify(operationId, session);
|
|
543
|
+
const written = session.writePreparedIfCurrent(prepared.plan);
|
|
544
|
+
if (written.kind !== 'written') return this.classify(operationId, session);
|
|
545
|
+
// Claude Code must now select the incoming login. Whatever keeps that from being proven -- its answer,
|
|
546
|
+
// a lost lock, a failed read -- is a failed proof: the exact outgoing bytes go back while the locks are
|
|
547
|
+
// provably still held and the handoff is aborted; if they cannot, the home is quarantined.
|
|
548
|
+
try { await session.proveEffectiveLogin(incomingLogin); }
|
|
549
|
+
catch (error) {
|
|
550
|
+
const reason = error instanceof ClaudeNativeEffectiveAuthUnsupportedError ? error.reason : null;
|
|
551
|
+
let restored = false;
|
|
552
|
+
try { restored = session.restorePreparedSource(prepared.plan); } catch { restored = false; }
|
|
553
|
+
return this.settle(operationId, restored ? 'aborted' : 'quarantined', written.snapshot.runningEffectiveAuth,
|
|
554
|
+
'unsupported_effective_auth', reason);
|
|
555
|
+
}
|
|
556
|
+
return this.classify(operationId, session, { targetProven: true });
|
|
523
557
|
});
|
|
524
558
|
} catch {
|
|
525
559
|
const outcome = await this.reconcileOperation(operationId);
|
|
@@ -17,8 +17,18 @@ interface IClaudeCodeLock {
|
|
|
17
17
|
interface IHeldLock {
|
|
18
18
|
lockfilePath: string;
|
|
19
19
|
release: () => Promise<void>;
|
|
20
|
+
/** After how long another process may take this lock over when its time is not renewed. */
|
|
21
|
+
stale: number;
|
|
22
|
+
/** The lock directory as it was created for this hold: a takeover removes it and creates another. */
|
|
23
|
+
identity: { device: number; inode: number; birthtimeMs: number };
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* How much of a lock's stale time must remain when a lease is asserted. Everything a caller does after an
|
|
28
|
+
* assertion is a synchronous file write, so no other process can find the lock stale before it completes.
|
|
29
|
+
*/
|
|
30
|
+
const HOLD_MARGIN_MS = 2_000;
|
|
31
|
+
|
|
22
32
|
export interface IClaudeCodeLocksOptions {
|
|
23
33
|
/** How long an operation waits for a lock that Claude Code holds. */
|
|
24
34
|
timeoutMs?: number;
|
|
@@ -26,6 +36,12 @@ export interface IClaudeCodeLocksOptions {
|
|
|
26
36
|
|
|
27
37
|
/** The vendor locks may be lost while an awaited database operation is in flight. */
|
|
28
38
|
export interface IClaudeCodeLockLease {
|
|
39
|
+
/**
|
|
40
|
+
* Proves, at this moment, that every lock is still this hold's: its directory is the one this hold created,
|
|
41
|
+
* and its time was renewed recently enough that no other process may take it over before a synchronous write
|
|
42
|
+
* that follows completes. A lock whose renewal stalled -- an event loop blocked past its update interval --
|
|
43
|
+
* fails this even when nobody has taken it yet.
|
|
44
|
+
*/
|
|
29
45
|
assertHeld(): void;
|
|
30
46
|
}
|
|
31
47
|
|
|
@@ -134,6 +150,16 @@ export class ClaudeCodeLocks {
|
|
|
134
150
|
const lease: IClaudeCodeLockLease = {
|
|
135
151
|
assertHeld: () => {
|
|
136
152
|
if (!active || compromised) throw new ClaudeCodeLockOutcomeUnknownError(false);
|
|
153
|
+
for (const lock of held) {
|
|
154
|
+
let current: plugins.fs.Stats;
|
|
155
|
+
try { current = plugins.fs.lstatSync(lock.lockfilePath); }
|
|
156
|
+
catch { throw new ClaudeCodeLockOutcomeUnknownError(false); }
|
|
157
|
+
if (!current.isDirectory() || current.dev !== lock.identity.device || current.ino !== lock.identity.inode
|
|
158
|
+
|| current.birthtimeMs !== lock.identity.birthtimeMs
|
|
159
|
+
|| Date.now() - current.mtimeMs > lock.stale - HOLD_MARGIN_MS) {
|
|
160
|
+
throw new ClaudeCodeLockOutcomeUnknownError(false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
137
163
|
},
|
|
138
164
|
};
|
|
139
165
|
const releaseAll = async (): Promise<void> => {
|
|
@@ -189,7 +215,11 @@ export class ClaudeCodeLocks {
|
|
|
189
215
|
const release = await lockfileArg.lock(lock.file, {
|
|
190
216
|
lockfilePath: lock.lockfilePath, realpath: false, stale: lock.stale, update: lock.update, onCompromised: onCompromisedArg,
|
|
191
217
|
});
|
|
192
|
-
|
|
218
|
+
let created: plugins.fs.Stats;
|
|
219
|
+
try { created = plugins.fs.lstatSync(lock.lockfilePath); }
|
|
220
|
+
catch (error) { await release().catch(() => undefined); throw error; }
|
|
221
|
+
heldArg.push({ lockfilePath: lock.lockfilePath, release, stale: lock.stale,
|
|
222
|
+
identity: { device: created.dev, inode: created.ino, birthtimeMs: created.birthtimeMs } });
|
|
193
223
|
} catch (error) {
|
|
194
224
|
if (errorCode(error) === 'ELOCKED') return lock;
|
|
195
225
|
if (requireAllArg || !lock.bestEffort) {
|