dsh-connect-workbuddy 2.0.6 → 2.0.10
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 +140 -0
- package/README.en.md +6 -1
- package/README.md +6 -1
- package/lib/bin.js +15 -2
- package/lib/client.js +416 -66
- package/lib/{host-heartbeat--iKDuwr5.js → host-heartbeat-DPbL9oi2.js} +539 -31
- package/lib/index.d.ts +438 -10
- package/lib/index.js +262 -27
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -31,6 +31,31 @@ interface WorkBuddyCredential {
|
|
|
31
31
|
*/
|
|
32
32
|
lastRefreshAtMs?: number;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Why one probed path did not yield an account.
|
|
36
|
+
*
|
|
37
|
+
* These are safe to surface: they carry paths and cause only, never token
|
|
38
|
+
* material. `encrypted` is the WorkBuddy-specific one — a build whose token
|
|
39
|
+
* fields are encrypted cannot be read at all unless the desktop app itself is
|
|
40
|
+
* present to hand over its at-rest key, so "sign in again" is the wrong advice
|
|
41
|
+
* for it (the user may be perfectly signed in).
|
|
42
|
+
*
|
|
43
|
+
* `wrong-region` is the second WorkBuddy-specific one, and it exists because of
|
|
44
|
+
* a real bug: a region-scoped store filters out every credential that belongs to
|
|
45
|
+
* the other region, so a candidate file holding a perfectly valid sign-in for
|
|
46
|
+
* the other tab was dropped by exactly the same `continue` that drops malformed
|
|
47
|
+
* files. The file then appeared in NO list at all — not an account (filtered by
|
|
48
|
+
* region) and not a failure (it parsed) — so a card could name two paths while
|
|
49
|
+
* its report listed one, and the user's actual, working sign-in was the one
|
|
50
|
+
* made invisible. Reporting it is also the most useful answer we can give: the
|
|
51
|
+
* user is signed in, and merely on the wrong tab.
|
|
52
|
+
*/
|
|
53
|
+
interface WorkBuddyCandidateFailure {
|
|
54
|
+
path: string;
|
|
55
|
+
source: 'desktop' | 'dsh';
|
|
56
|
+
reason: 'missing' | 'unreadable' | 'invalid' | 'encrypted' | 'wrong-region';
|
|
57
|
+
message?: string;
|
|
58
|
+
}
|
|
34
59
|
/** Read-only sign-in summary for status and doctor output. */
|
|
35
60
|
interface WorkBuddyAuthStatus {
|
|
36
61
|
state: 'signed-in' | 'signed-out';
|
|
@@ -69,13 +94,25 @@ interface WorkBuddyStoreOptions {
|
|
|
69
94
|
refresh: (credential: WorkBuddyCredential) => Promise<WorkBuddyRefreshOutcome>;
|
|
70
95
|
/** Refresh this long before actual expiry; default five minutes. */
|
|
71
96
|
refreshMarginMs?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the desktop app's at-rest field key, used to open documents whose
|
|
99
|
+
* token fields are encrypted (Windows builds). Defaults to asking the
|
|
100
|
+
* installed app; injectable so the encrypted path is testable.
|
|
101
|
+
*/
|
|
102
|
+
resolveAtRestKey?: () => Promise<Buffer | undefined>;
|
|
72
103
|
}
|
|
73
104
|
/** One selectable local account, token-free. */
|
|
74
105
|
interface WorkBuddyAccountChoice {
|
|
75
106
|
/** Stable id derived from `uin` (or `uid` when uin is absent). */
|
|
76
107
|
id: string;
|
|
108
|
+
/**
|
|
109
|
+
* The account's human name, or `''` when the desktop app recorded none.
|
|
110
|
+
*
|
|
111
|
+
* Never an identifier: `uin`/`uid` are opaque numbers and UUIDs, and using one
|
|
112
|
+
* as a display name reads as "the plugin does not know who this is". Callers
|
|
113
|
+
* that render a name choose their own placeholder for the empty case.
|
|
114
|
+
*/
|
|
77
115
|
accountName: string;
|
|
78
|
-
uin?: string;
|
|
79
116
|
domain: string;
|
|
80
117
|
source: 'desktop' | 'dsh';
|
|
81
118
|
tokenExpiresAtMs: number;
|
|
@@ -119,8 +156,24 @@ declare function defaultDesktopAuthPath(): string | undefined;
|
|
|
119
156
|
* Parse a WorkBuddy auth document in either on-disk shape: the plugin OAuth
|
|
120
157
|
* nested form `{"auth":{...},"account":{...}}` and the flat panel form.
|
|
121
158
|
* Returns undefined when the document carries no access token.
|
|
159
|
+
*
|
|
160
|
+
* `atRestKey` is required to read documents whose fields are encrypted
|
|
161
|
+
* (see {@link credentialField}); pass the key obtained from
|
|
162
|
+
* `readAtRestKey()` when the plain read reports no token.
|
|
163
|
+
*/
|
|
164
|
+
declare function parseWorkBuddyAuth(text: string, filePath: string, atRestKey?: Buffer): WorkBuddyCredential | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* Whether a document carries any field the reader must decrypt — the token
|
|
167
|
+
* fields or the identity fields — i.e. whether reading it needs the at-rest key
|
|
168
|
+
* at all.
|
|
169
|
+
*
|
|
170
|
+
* Deciding this BEFORE asking the app for its key keeps the plain case (macOS
|
|
171
|
+
* and older Windows builds, and every plugin-owned copy) from paying for a
|
|
172
|
+
* child process on every credential read. Identity fields are included because
|
|
173
|
+
* a future build could encrypt the display name while leaving tokens plain;
|
|
174
|
+
* gating on tokens alone would then silently drop the name again.
|
|
122
175
|
*/
|
|
123
|
-
declare function
|
|
176
|
+
declare function hasEncryptedCredentialFields(text: string): boolean;
|
|
124
177
|
/**
|
|
125
178
|
* Filename of a path regardless of the host separator: Windows paths use `\`
|
|
126
179
|
* and this helper must keep working when a Windows path is compared on a
|
|
@@ -150,6 +203,7 @@ declare class WorkBuddyCredentialStore {
|
|
|
150
203
|
private readonly legacyOwnPath;
|
|
151
204
|
private readonly legacyOwnPathExplicit;
|
|
152
205
|
private readonly authDirs;
|
|
206
|
+
private readonly resolveAtRestKey;
|
|
153
207
|
private desktopPathOverride;
|
|
154
208
|
private accountId;
|
|
155
209
|
private inflight;
|
|
@@ -262,6 +316,16 @@ declare class WorkBuddyCredentialStore {
|
|
|
262
316
|
selectionLost(): Promise<boolean>;
|
|
263
317
|
/** The freshest stored credential for the current selection, no refresh. */
|
|
264
318
|
current(): Promise<WorkBuddyCredential | undefined>;
|
|
319
|
+
/**
|
|
320
|
+
* One account's stored credential by id, WITHOUT consulting or changing the
|
|
321
|
+
* current selection.
|
|
322
|
+
*
|
|
323
|
+
* Exists so a recovery probe can test whether some OTHER local account is
|
|
324
|
+
* still accepted upstream while a rejected one stays selected: mutating the
|
|
325
|
+
* live selection to find that out would be exactly the silent account switch
|
|
326
|
+
* this store refuses to perform (it would bill the wrong account mid-probe).
|
|
327
|
+
*/
|
|
328
|
+
credentialFor(accountId: string): Promise<WorkBuddyCredential | undefined>;
|
|
265
329
|
/** The credential to send upstream: {@link current}, refreshed on demand. */
|
|
266
330
|
resolve(): Promise<WorkBuddyCredential>;
|
|
267
331
|
/** Read-only sign-in summary; never refreshes and never throws. */
|
|
@@ -284,6 +348,21 @@ declare class WorkBuddyCredentialStore {
|
|
|
284
348
|
private readOwns;
|
|
285
349
|
/** Whether any candidate file exists as a regular file; diagnostics only. */
|
|
286
350
|
desktopFilePresent(): Promise<boolean>;
|
|
351
|
+
/**
|
|
352
|
+
* Which paths were probed and why each one yielded no credential.
|
|
353
|
+
*
|
|
354
|
+
* Read-only and token-free: it exists so a signed-out card can explain
|
|
355
|
+
* itself. A bare "not signed in" is undiagnosable on a machine whose layout
|
|
356
|
+
* differs from the ones this plugin was written against — and on Windows it
|
|
357
|
+
* is actively misleading, because encrypted token fields need the desktop app
|
|
358
|
+
* present to be read at all. Only paths this store actually consults are
|
|
359
|
+
* reported, and only files that failed: one healthy sibling would make the
|
|
360
|
+
* whole list noise.
|
|
361
|
+
*/
|
|
362
|
+
diagnose(): Promise<{
|
|
363
|
+
tried: string[];
|
|
364
|
+
failures: WorkBuddyCandidateFailure[];
|
|
365
|
+
}>;
|
|
287
366
|
}
|
|
288
367
|
//#endregion
|
|
289
368
|
//#region src/upstream.d.ts
|
|
@@ -395,6 +474,28 @@ declare function regionOf(domain: string): WorkBuddyRegion;
|
|
|
395
474
|
* `tool_choice` (the upstream's field is a string; object forms return 400).
|
|
396
475
|
*/
|
|
397
476
|
declare function prepareChatBody(source: string): string;
|
|
477
|
+
/** Marker carried by {@link WorkBuddyCredentialRejectedError}; survives bundling. */
|
|
478
|
+
declare const CREDENTIAL_REJECTED_CODE = "WORKBUDDY_CREDENTIAL_REJECTED";
|
|
479
|
+
/**
|
|
480
|
+
* The upstream refused the CREDENTIAL itself rather than failing the request.
|
|
481
|
+
*
|
|
482
|
+
* This is a distinct, actionable class: the token is not usable and no retry
|
|
483
|
+
* with the same token will help. Callers use it to tell the user what to do
|
|
484
|
+
* about it (switch accounts, or sign in again) instead of showing a raw HTTP
|
|
485
|
+
* error, and the card must never confuse it with a transient upstream fault.
|
|
486
|
+
*
|
|
487
|
+
* Identified by {@link CREDENTIAL_REJECTED_CODE} rather than `instanceof`, so
|
|
488
|
+
* the check keeps working when the caller and the thrower end up in different
|
|
489
|
+
* module instances (bundled host half vs. a test's source import).
|
|
490
|
+
*/
|
|
491
|
+
declare class WorkBuddyCredentialRejectedError extends Error {
|
|
492
|
+
readonly code = "WORKBUDDY_CREDENTIAL_REJECTED";
|
|
493
|
+
/** HTTP status the upstream answered with (401 or 403). */
|
|
494
|
+
readonly status: number;
|
|
495
|
+
constructor(status: number);
|
|
496
|
+
}
|
|
497
|
+
/** Whether an error reports that the upstream refused the credential itself. */
|
|
498
|
+
declare function isCredentialRejectedError(value: unknown): value is WorkBuddyCredentialRejectedError;
|
|
398
499
|
/**
|
|
399
500
|
* Parse the upstream's `credits` string into a multiplier.
|
|
400
501
|
*
|
|
@@ -496,6 +597,18 @@ declare class WorkBuddyCatalog {
|
|
|
496
597
|
* serving the fallback rather than blanking a region that may be fine.
|
|
497
598
|
*/
|
|
498
599
|
private usable;
|
|
600
|
+
/**
|
|
601
|
+
* Whether the user has switched this region's provider ON. Opt-out: defaults
|
|
602
|
+
* to `true`, so a config predating this switch (and the pre-region-split flat
|
|
603
|
+
* fields, which never carry `enabled`) keeps both providers running. Set to
|
|
604
|
+
* `false` by `setRegionEnabled` to withdraw the region from DSH's model
|
|
605
|
+
* picker entirely (its adapter route and configurable-provider entry are
|
|
606
|
+
* pulled by the Host via `AdapterRegistrationHandle.replace([])` /
|
|
607
|
+
* `DirectoryRegistrationHandle.replace([])`). This is a USER choice, distinct
|
|
608
|
+
* from {@link usable}: a region the user switched off keeps its catalog,
|
|
609
|
+
* accounts, and model picks intact and reappears the moment they re-check it.
|
|
610
|
+
*/
|
|
611
|
+
private enabled;
|
|
499
612
|
/**
|
|
500
613
|
* @param region Seeds the static fallback for this region; each region's
|
|
501
614
|
* provider must never serve the other region's roster before its first
|
|
@@ -505,15 +618,19 @@ declare class WorkBuddyCatalog {
|
|
|
505
618
|
/**
|
|
506
619
|
* Current entries; the fallback list until the upstream answer lands.
|
|
507
620
|
*
|
|
508
|
-
* Empty while this region has no local sign-in
|
|
509
|
-
* registered (its row still hosts the settings card
|
|
510
|
-
* but it advertises nothing to pick. DSH drops empty
|
|
511
|
-
* the picker (`buildModelCatalog` filters
|
|
512
|
-
* disappears exactly when it would be pure
|
|
621
|
+
* Empty while this region has no local sign-in OR the user has switched it
|
|
622
|
+
* off: the provider stays registered (its row still hosts the settings card
|
|
623
|
+
* and the account picker), but it advertises nothing to pick. DSH drops empty
|
|
624
|
+
* provider groups from the picker (`buildModelCatalog` filters
|
|
625
|
+
* `models.length > 0`), so the group disappears exactly when it would be pure
|
|
626
|
+
* noise. The two empty causes are independent: `usable` is the automatic
|
|
627
|
+
* "no account" gate (issue #12), `enabled` is the user's explicit on/off.
|
|
513
628
|
*/
|
|
514
629
|
current(): readonly WorkBuddyModelInfo[];
|
|
515
630
|
/** Whether this region currently advertises any model. */
|
|
516
631
|
isRegionUsable(): boolean;
|
|
632
|
+
/** Whether the user has this region's provider switched on. */
|
|
633
|
+
isRegionEnabled(): boolean;
|
|
517
634
|
/**
|
|
518
635
|
* Record whether this region has a local sign-in.
|
|
519
636
|
*
|
|
@@ -526,6 +643,16 @@ declare class WorkBuddyCatalog {
|
|
|
526
643
|
* its adapter snapshots when nothing moved.
|
|
527
644
|
*/
|
|
528
645
|
setRegionUsable(usable: boolean): boolean;
|
|
646
|
+
/**
|
|
647
|
+
* Record the user's explicit on/off choice for this region's provider.
|
|
648
|
+
*
|
|
649
|
+
* Distinct from {@link setRegionUsable}: that is the automatic "no local
|
|
650
|
+
* account" gate, while this is a deliberate user action (issue #11-style
|
|
651
|
+
* region switch). The Host reads the same `enabled` flag through
|
|
652
|
+
* `regionStateOf(config, region).enabled !== false`, so the two halves never
|
|
653
|
+
* disagree. Returns whether the value changed.
|
|
654
|
+
*/
|
|
655
|
+
setRegionEnabled(enabled: boolean): boolean;
|
|
529
656
|
/** Replace the list; callers invalidate their adapter snapshot after this. */
|
|
530
657
|
set(models: readonly WorkBuddyModelInfo[]): void;
|
|
531
658
|
}
|
|
@@ -622,6 +749,170 @@ declare function workBuddyThinkingLevelMap(info: WorkBuddyModelInfo): WorkBuddyT
|
|
|
622
749
|
*/
|
|
623
750
|
declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
|
|
624
751
|
//#endregion
|
|
752
|
+
//#region src/at-rest.d.ts
|
|
753
|
+
/**
|
|
754
|
+
* WorkBuddy desktop "at-rest" credential decryption.
|
|
755
|
+
*
|
|
756
|
+
* The WorkBuddy desktop app (2.x, Windows builds first) no longer stores
|
|
757
|
+
* `auth.accessToken` / `auth.refreshToken` as plain strings. It now writes a
|
|
758
|
+
* field wrapper:
|
|
759
|
+
*
|
|
760
|
+
* { "$wbEncrypted": 1, "envelope": "<base64 of a JSON envelope>" }
|
|
761
|
+
*
|
|
762
|
+
* where the envelope is `{suite, keyId, nonce, authTag, ciphertext}` for
|
|
763
|
+
* AES-256-GCM with a 12-byte nonce and a 16-byte tag. The authenticated
|
|
764
|
+
* additional data is a length-prefixed transcript over the scheme, suite,
|
|
765
|
+
* keyId and framing, so the ciphertext can only be opened for the exact field
|
|
766
|
+
* shape it was sealed for.
|
|
767
|
+
*
|
|
768
|
+
* The field key itself is NOT a user secret: it is a build-time constant
|
|
769
|
+
* compiled into the app's own Electron native module
|
|
770
|
+
* (`electron_browser_workbuddy_storage`). The app fetches it through
|
|
771
|
+
* `loggerGet()` and hashes the returned base64 STRING (not the decoded bytes)
|
|
772
|
+
* to obtain the 32-byte key; `keyId` is the first 16 hex characters of that
|
|
773
|
+
* key's SHA-256.
|
|
774
|
+
*
|
|
775
|
+
* This module re-derives the same key by asking the installed app for the same
|
|
776
|
+
* payload, and caches it in memory for the process lifetime. Nothing is ever
|
|
777
|
+
* written to disk, and the payload is never logged.
|
|
778
|
+
*
|
|
779
|
+
* @module dsh-connect-workbuddy/at-rest
|
|
780
|
+
*/
|
|
781
|
+
/** A `{$wbEncrypted:1,envelope}` field wrapper, the only shape this module opens. */
|
|
782
|
+
interface WorkBuddyEncryptedField {
|
|
783
|
+
$wbEncrypted: 1;
|
|
784
|
+
envelope: string;
|
|
785
|
+
}
|
|
786
|
+
/** Env override pointing at the WorkBuddy desktop executable. */
|
|
787
|
+
declare const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
|
|
788
|
+
/** Whether a value is the app's encrypted-field wrapper. */
|
|
789
|
+
declare function isEncryptedFieldWrapper(value: unknown): value is WorkBuddyEncryptedField;
|
|
790
|
+
/**
|
|
791
|
+
* The at-rest key id for a derived 32-byte key: the first 16 hex characters of
|
|
792
|
+
* its SHA-256. This is what the envelope's `keyId` is checked against, so a
|
|
793
|
+
* mismatched key fails loudly instead of returning garbage.
|
|
794
|
+
*/
|
|
795
|
+
declare function deriveAtRestKeyId(key: Buffer): string;
|
|
796
|
+
/**
|
|
797
|
+
* Derive the 32-byte field key from the app's key payload JSON.
|
|
798
|
+
*
|
|
799
|
+
* The app hashes the payload's base64 STRING — not its decoded bytes — so the
|
|
800
|
+
* same spelling is required here; hashing the decoded secret would produce a
|
|
801
|
+
* different key and every field would fail to open.
|
|
802
|
+
*/
|
|
803
|
+
declare function deriveAtRestKey(payloadJson: string): Buffer;
|
|
804
|
+
/**
|
|
805
|
+
* Open one encrypted field with a derived key and return its plaintext.
|
|
806
|
+
*
|
|
807
|
+
* Throws when the envelope is malformed, belongs to another key, or fails
|
|
808
|
+
* authentication — a GCM tag mismatch is the signal that the transcript or the
|
|
809
|
+
* key is wrong, and it must never degrade into a truncated token.
|
|
810
|
+
*/
|
|
811
|
+
declare function openEncryptedField(field: WorkBuddyEncryptedField, key: Buffer): string;
|
|
812
|
+
/**
|
|
813
|
+
* Candidate paths of the WorkBuddy desktop executable, in probe order.
|
|
814
|
+
*
|
|
815
|
+
* The Windows build is the one that encrypts credentials, so Windows leads;
|
|
816
|
+
* the macOS bundle is listed because the same native module ships there and a
|
|
817
|
+
* future build may enable the policy, and `undefined` entries (an unset env
|
|
818
|
+
* variable) are dropped.
|
|
819
|
+
*/
|
|
820
|
+
declare function workbuddyAppExecutableCandidates(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string[];
|
|
821
|
+
/**
|
|
822
|
+
* The first candidate that exists as a file, or undefined when the desktop app
|
|
823
|
+
* is not installed where this platform expects it.
|
|
824
|
+
*/
|
|
825
|
+
declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
826
|
+
/**
|
|
827
|
+
* Ask the installed desktop app for its key payload by running its own binary
|
|
828
|
+
* as plain Node (`ELECTRON_RUN_AS_NODE`) and calling the native binding.
|
|
829
|
+
*
|
|
830
|
+
* The binding is the app's own public surface for this value, so the plugin
|
|
831
|
+
* never has to carry a copy of a build-specific constant: it asks the very
|
|
832
|
+
* build that wrote the file. The child is given no stdin and a hard timeout,
|
|
833
|
+
* and its stdout is the only thing read.
|
|
834
|
+
*/
|
|
835
|
+
declare function fetchAtRestKeyPayload(executable: string): Promise<string>;
|
|
836
|
+
/**
|
|
837
|
+
* The desktop app's at-rest field key, or undefined when it cannot be obtained
|
|
838
|
+
* (app not installed, an older build without the native module, or a future
|
|
839
|
+
* build that rotates the payload). Cached after the first success so the app is
|
|
840
|
+
* spawned at most once per process; a failure is retried on the next call,
|
|
841
|
+
* because the user may install or start the app between reads.
|
|
842
|
+
*/
|
|
843
|
+
declare function readAtRestKey(): Promise<Buffer | undefined>;
|
|
844
|
+
/** Drop the cached key; tests and diagnostics only. */
|
|
845
|
+
declare function clearAtRestKeyCache(): void;
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/credential-recovery.d.ts
|
|
848
|
+
/** What the user can do about a refused credential. */
|
|
849
|
+
interface WorkBuddyCredentialRecovery {
|
|
850
|
+
/**
|
|
851
|
+
* Another local account answered the upstream during this recovery, so
|
|
852
|
+
* switching to it is a fix that has been verified — not a guess.
|
|
853
|
+
*/
|
|
854
|
+
usableAccount?: {
|
|
855
|
+
accountId: string;
|
|
856
|
+
accountName: string;
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* Whether signing in again is the only remaining fix: no other local account
|
|
860
|
+
* exists at all, so there is nothing to switch to.
|
|
861
|
+
*/
|
|
862
|
+
reloginRequired: boolean;
|
|
863
|
+
}
|
|
864
|
+
/** One account as the recovery path needs to see it. */
|
|
865
|
+
interface WorkBuddyRecoveryCandidate {
|
|
866
|
+
id: string;
|
|
867
|
+
accountName: string;
|
|
868
|
+
}
|
|
869
|
+
/** Reads one account's credential for probing, without changing the selection. */
|
|
870
|
+
interface WorkBuddyProbeStore {
|
|
871
|
+
accounts(): Promise<readonly WorkBuddyRecoveryCandidate[]>;
|
|
872
|
+
credentialFor(accountId: string): Promise<WorkBuddyCredential | undefined>;
|
|
873
|
+
}
|
|
874
|
+
/** Constructor options for {@link createAccountUsabilityProbe}. */
|
|
875
|
+
interface WorkBuddyUsabilityProbeOptions {
|
|
876
|
+
/** The credential store for one region (its selection is never touched). */
|
|
877
|
+
store(region: WorkBuddyRegion): WorkBuddyProbeStore;
|
|
878
|
+
/** The upstream client; only its cheap read-only calls are used. */
|
|
879
|
+
client: Pick<WorkBuddyUpstreamClient, 'fetchCheckinStatus'>;
|
|
880
|
+
/**
|
|
881
|
+
* How long a probe verdict is reused. The card polls the usage route, so
|
|
882
|
+
* without this a broken selection would re-probe every account on every poll.
|
|
883
|
+
*/
|
|
884
|
+
ttlMs?: number;
|
|
885
|
+
/** Injectable clock, for tests. */
|
|
886
|
+
now?: () => number;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Cached "is this account still accepted upstream?" probe.
|
|
890
|
+
*
|
|
891
|
+
* The cache key includes the credential's own `lastRefreshAtMs`, so a fresh
|
|
892
|
+
* sign-in (which rewrites the file's issuance time) invalidates a stale verdict
|
|
893
|
+
* automatically — a rejection recorded before the user signed in again cannot
|
|
894
|
+
* keep labelling the account unusable afterwards, and no explicit invalidation
|
|
895
|
+
* is needed.
|
|
896
|
+
*/
|
|
897
|
+
declare function createAccountUsabilityProbe(options: WorkBuddyUsabilityProbeOptions): (region: WorkBuddyRegion, account: WorkBuddyRecoveryCandidate) => Promise<boolean>;
|
|
898
|
+
/**
|
|
899
|
+
* Decide what to tell the user after the upstream refused the selected
|
|
900
|
+
* credential: switch to a verified-usable account, or sign in again.
|
|
901
|
+
*
|
|
902
|
+
* The two answers are mutually exclusive by construction. `reloginRequired` is
|
|
903
|
+
* set ONLY when there is no other local account to switch to — never as a
|
|
904
|
+
* fallback for "the probe did not verify anything", because that would repeat
|
|
905
|
+
* the misdirection this module exists to remove. When other accounts exist but
|
|
906
|
+
* none was verified, both fields stay empty and the card says so honestly.
|
|
907
|
+
*/
|
|
908
|
+
declare function resolveCredentialRecovery(options: {
|
|
909
|
+
region: WorkBuddyRegion;
|
|
910
|
+
store: WorkBuddyProbeStore;
|
|
911
|
+
rejectedAccountId?: string;
|
|
912
|
+
/** Verifies whether one account is still accepted; see the probe factory. */
|
|
913
|
+
probe: (region: WorkBuddyRegion, account: WorkBuddyRecoveryCandidate) => Promise<boolean>;
|
|
914
|
+
}): Promise<WorkBuddyCredentialRecovery>;
|
|
915
|
+
//#endregion
|
|
625
916
|
//#region src/host-heartbeat.d.ts
|
|
626
917
|
/**
|
|
627
918
|
* Host-side heartbeat: a small JSON file written under `$DSH_HOME` once the
|
|
@@ -725,6 +1016,36 @@ declare function withWorkBuddyRegion(path: string, region: WorkBuddyWebRegion):
|
|
|
725
1016
|
* answer 400 instead of guessing.
|
|
726
1017
|
*/
|
|
727
1018
|
declare function regionOfStatusUrl(url: string): WorkBuddyWebRegion | undefined;
|
|
1019
|
+
/**
|
|
1020
|
+
* Whether one region's provider is switched on. Opt-out semantics: only an
|
|
1021
|
+
* explicit `false` disables it, so a config written before this switch existed
|
|
1022
|
+
* (and the pre-region-split flat fields, which never carry `enabled`) keep both
|
|
1023
|
+
* providers running exactly as before. The Host reads the same rule through
|
|
1024
|
+
* `regionStateOf`, so card and Host can never disagree about a region's state.
|
|
1025
|
+
*
|
|
1026
|
+
* `value` may be the whole settings section or the `regions` map (see
|
|
1027
|
+
* {@link regionsMapOf}).
|
|
1028
|
+
*/
|
|
1029
|
+
declare function regionEnabledOf(value: unknown, region: WorkBuddyWebRegion): boolean;
|
|
1030
|
+
/** Build the next `regions` settings value for a signed-in tab's save. */
|
|
1031
|
+
declare function nextRegionSlots<Slot extends object>(regions: unknown, region: WorkBuddyWebRegion, slot: Slot): Record<string, unknown>;
|
|
1032
|
+
/**
|
|
1033
|
+
* Build the next `regions` settings value for a provider on/off toggle. ONLY
|
|
1034
|
+
* the target region's `enabled` flag changes: every other field of that slot
|
|
1035
|
+
* (its directory, selection, image opt-ins, context budgets) and every other
|
|
1036
|
+
* region's slot are carried over verbatim, so switching a provider off never
|
|
1037
|
+
* discards the user's model picks and switching it back on restores them.
|
|
1038
|
+
*
|
|
1039
|
+
* This is deliberately separate from {@link nextRegionSlots}: that helper
|
|
1040
|
+
* writes a whole slot from a signed-in tab's draft, while this one must work
|
|
1041
|
+
* for a region that is signed OUT — which is precisely the region a user wants
|
|
1042
|
+
* to switch off (no international install, no international account).
|
|
1043
|
+
*
|
|
1044
|
+
* `value` may be the whole settings section or the `regions` map; the RETURN
|
|
1045
|
+
* value is always the `regions` map, i.e. exactly what `settingsScope.set(
|
|
1046
|
+
* 'regions', ...)` needs.
|
|
1047
|
+
*/
|
|
1048
|
+
declare function nextRegionEnabled(value: unknown, region: WorkBuddyWebRegion, enabled: boolean): Record<string, unknown>;
|
|
728
1049
|
/** One credit package as the upstream returns it, node-free. */
|
|
729
1050
|
interface WorkBuddyWebCreditPackage {
|
|
730
1051
|
packageName: string;
|
|
@@ -759,6 +1080,30 @@ interface WorkBuddyWebCheckin {
|
|
|
759
1080
|
streakBonusCredit: number;
|
|
760
1081
|
claimButtonText?: string;
|
|
761
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* What the user can do about a credential the upstream refused.
|
|
1085
|
+
*
|
|
1086
|
+
* Two different fixes hide behind one 401, and naming the wrong one is worse
|
|
1087
|
+
* than saying nothing: with a stale account SELECTED but a healthy sign-in
|
|
1088
|
+
* sitting right there, "sign in again" sends the user to re-authenticate — which
|
|
1089
|
+
* changes nothing, because the credentials were never the problem.
|
|
1090
|
+
*/
|
|
1091
|
+
interface WorkBuddyWebRecovery {
|
|
1092
|
+
/**
|
|
1093
|
+
* Another local account answered the upstream during recovery, so switching
|
|
1094
|
+
* to it is a VERIFIED fix rather than a suggestion.
|
|
1095
|
+
*/
|
|
1096
|
+
usableAccount?: {
|
|
1097
|
+
accountId: string;
|
|
1098
|
+
accountName: string;
|
|
1099
|
+
};
|
|
1100
|
+
/**
|
|
1101
|
+
* Signing in again is the only remaining fix: this region has no other local
|
|
1102
|
+
* account to switch to. Set only in that case — never as a fallback for
|
|
1103
|
+
* "nothing could be verified".
|
|
1104
|
+
*/
|
|
1105
|
+
reloginRequired: boolean;
|
|
1106
|
+
}
|
|
762
1107
|
/** Editable WorkBuddy model row rendered by the plugin-owned settings card. */
|
|
763
1108
|
interface WorkBuddyWebModel {
|
|
764
1109
|
id: string;
|
|
@@ -789,14 +1134,39 @@ declare function toPersistedWorkBuddyModel(model: WorkBuddyWebModel): Omit<WorkB
|
|
|
789
1134
|
/** One selectable local account, token-free. */
|
|
790
1135
|
interface WorkBuddyWebAccount {
|
|
791
1136
|
id: string;
|
|
1137
|
+
/**
|
|
1138
|
+
* The account's human name, or `''` when the desktop app recorded none.
|
|
1139
|
+
* The card renders its own placeholder for the empty case.
|
|
1140
|
+
*/
|
|
792
1141
|
accountName: string;
|
|
793
|
-
uin?: string;
|
|
794
1142
|
domain: string;
|
|
795
1143
|
source: 'desktop' | 'dsh';
|
|
796
1144
|
tokenExpiresAtMs: number;
|
|
797
1145
|
selected: boolean;
|
|
798
1146
|
}
|
|
799
1147
|
type WorkBuddyWebPackage = WorkBuddyWebCreditPackage;
|
|
1148
|
+
/**
|
|
1149
|
+
* One probed candidate path and why it yielded no account.
|
|
1150
|
+
*
|
|
1151
|
+
* Safe to send to the browser: a path, its source, and a cause. No token
|
|
1152
|
+
* material, and no content read out of the files beyond an error string.
|
|
1153
|
+
*
|
|
1154
|
+
* `encrypted` exists because WorkBuddy (unlike a plain JSON-token app) can
|
|
1155
|
+
* refuse a correctly signed-in user: Windows builds encrypt the token fields,
|
|
1156
|
+
* and opening them needs the desktop app present to hand over its at-rest key.
|
|
1157
|
+
* Telling that user to "sign in again" is wrong — they already are.
|
|
1158
|
+
*
|
|
1159
|
+
* `wrong-region` is the other WorkBuddy-specific one: the file holds a valid
|
|
1160
|
+
* sign-in for the OTHER tab's region, so this tab filters it out of the account
|
|
1161
|
+
* list. Without this entry the file was reported nowhere at all, which both
|
|
1162
|
+
* undercounted "paths checked" and hid the user's real sign-in.
|
|
1163
|
+
*/
|
|
1164
|
+
interface WorkBuddyWebSearchPath {
|
|
1165
|
+
path: string;
|
|
1166
|
+
source: 'desktop' | 'dsh';
|
|
1167
|
+
reason: 'missing' | 'unreadable' | 'invalid' | 'encrypted' | 'wrong-region';
|
|
1168
|
+
message?: string;
|
|
1169
|
+
}
|
|
800
1170
|
/**
|
|
801
1171
|
* Region of the signed-in credential: the CN app (`codebuddy.cn` /
|
|
802
1172
|
* `workbuddy.cn`) or the international WorkBuddy AI app (`workbuddy.ai`).
|
|
@@ -822,19 +1192,36 @@ type WorkBuddyWebUsage =
|
|
|
822
1192
|
{
|
|
823
1193
|
status: 'signed-out';
|
|
824
1194
|
accounts: readonly WorkBuddyWebAccount[];
|
|
1195
|
+
/** Whether this region's provider is currently offered to DSH. */
|
|
1196
|
+
enabled?: boolean;
|
|
825
1197
|
message?: string;
|
|
826
1198
|
/** The persisted account id matches no local account. */
|
|
827
1199
|
selectionLost?: boolean;
|
|
828
1200
|
/** A saved per-region choice is in effect (false = following the app). */
|
|
829
1201
|
selectionExplicit: boolean;
|
|
1202
|
+
/**
|
|
1203
|
+
* The paths this region's store probed, and why each yielded nothing.
|
|
1204
|
+
*
|
|
1205
|
+
* Present only on the "nothing was found at all" branch — it is the
|
|
1206
|
+
* explanation for a state that is otherwise a dead end. A missing candidate
|
|
1207
|
+
* (`missing`) is normal noise on any machine and the card filters those out
|
|
1208
|
+
* of its default view; the interesting ones are `unreadable`, `invalid`,
|
|
1209
|
+
* and especially `encrypted`.
|
|
1210
|
+
*/
|
|
1211
|
+
searched?: readonly WorkBuddyWebSearchPath[];
|
|
830
1212
|
} | {
|
|
831
1213
|
status: 'signed-in';
|
|
832
1214
|
accountId: string;
|
|
1215
|
+
/**
|
|
1216
|
+
* The account's human name, or `''` when the desktop app recorded none.
|
|
1217
|
+
* Never an identifier — see {@link WorkBuddyWebAccount.accountName}.
|
|
1218
|
+
*/
|
|
833
1219
|
accountName: string;
|
|
834
|
-
uin?: string;
|
|
835
1220
|
domain?: string;
|
|
836
1221
|
/** Which per-region model directory and selection this account owns. */
|
|
837
1222
|
region: WorkBuddyWebRegion;
|
|
1223
|
+
/** Whether this region's provider is currently offered to DSH. */
|
|
1224
|
+
enabled?: boolean;
|
|
838
1225
|
source?: 'desktop' | 'dsh';
|
|
839
1226
|
tokenExpiresAtMs: number;
|
|
840
1227
|
/** A saved per-region choice is in effect (false = following the app). */
|
|
@@ -847,6 +1234,14 @@ type WorkBuddyWebUsage =
|
|
|
847
1234
|
creditsError?: string;
|
|
848
1235
|
checkin?: WorkBuddyWebCheckin;
|
|
849
1236
|
checkinError?: string;
|
|
1237
|
+
/**
|
|
1238
|
+
* The upstream refused the credential itself (a 401/403), as opposed to a
|
|
1239
|
+
* transient upstream fault. Distinguishes "your token is not usable" from
|
|
1240
|
+
* "the request failed", which need opposite advice.
|
|
1241
|
+
*/
|
|
1242
|
+
credentialRejected?: boolean;
|
|
1243
|
+
/** Present only alongside {@link credentialRejected}. */
|
|
1244
|
+
recovery?: WorkBuddyWebRecovery;
|
|
850
1245
|
} | {
|
|
851
1246
|
status: 'error';
|
|
852
1247
|
message: string;
|
|
@@ -873,6 +1268,12 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
873
1268
|
contextBudgets(region: WorkBuddyRegion): Readonly<Record<string, number | undefined>>;
|
|
874
1269
|
/** Re-read the live catalog of one region from the upstream. */
|
|
875
1270
|
discoverModels?(region: WorkBuddyRegion, signal?: AbortSignal): Promise<readonly WorkBuddyModelInfo[]>;
|
|
1271
|
+
/**
|
|
1272
|
+
* Whether the requested region's provider is currently offered to DSH. The
|
|
1273
|
+
* card renders this as the tab's on/off checkbox, so the switch reflects the
|
|
1274
|
+
* committed settings value rather than a local guess.
|
|
1275
|
+
*/
|
|
1276
|
+
regionEnabled(region: WorkBuddyRegion): boolean;
|
|
876
1277
|
/**
|
|
877
1278
|
* Report whether a region still has at least one local sign-in.
|
|
878
1279
|
*
|
|
@@ -883,6 +1284,15 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
883
1284
|
* no scan of its own.
|
|
884
1285
|
*/
|
|
885
1286
|
regionUsable?(region: WorkBuddyRegion, usable: boolean): void;
|
|
1287
|
+
/**
|
|
1288
|
+
* Verify whether one local account is still accepted by the upstream.
|
|
1289
|
+
*
|
|
1290
|
+
* Consulted ONLY after the selected credential has been refused, to tell
|
|
1291
|
+
* "switch accounts" apart from "sign in again". Left undefined, nothing is
|
|
1292
|
+
* claimed usable and the card falls back to the re-login advice that needs no
|
|
1293
|
+
* probe.
|
|
1294
|
+
*/
|
|
1295
|
+
accountUsable?(region: WorkBuddyRegion, account: WorkBuddyRecoveryCandidate): Promise<boolean>;
|
|
886
1296
|
}
|
|
887
1297
|
/**
|
|
888
1298
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
@@ -908,6 +1318,15 @@ declare const inject: string[];
|
|
|
908
1318
|
declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
|
|
909
1319
|
/** One region's model directory and the user's selection within it. */
|
|
910
1320
|
interface WorkBuddyRegionState {
|
|
1321
|
+
/**
|
|
1322
|
+
* Whether this region's provider is switched on. Opt-out: only an explicit
|
|
1323
|
+
* `false` disables it, so a config predating this switch keeps both providers
|
|
1324
|
+
* running. A disabled region is fully withdrawn from the harness — its adapter
|
|
1325
|
+
* route and its configurable-provider entry both hold zero routes, so it
|
|
1326
|
+
* disappears from DSH's model picker instead of lingering as an unselectable
|
|
1327
|
+
* row (see `syncRegionRegistration`).
|
|
1328
|
+
*/
|
|
1329
|
+
enabled?: boolean;
|
|
911
1330
|
/** The last-refreshed directory for this region; what the card displays. */
|
|
912
1331
|
lastCatalog?: WorkBuddyModelInfo[];
|
|
913
1332
|
/** The user's selection in this region, as model ids. */
|
|
@@ -964,6 +1383,15 @@ declare const Config: z<Config>;
|
|
|
964
1383
|
* picks.
|
|
965
1384
|
*/
|
|
966
1385
|
declare function regionStateOf(config: Config, region: WorkBuddyRegion): WorkBuddyRegionState;
|
|
1386
|
+
/**
|
|
1387
|
+
* Whether one region's provider is switched on. Opt-out semantics: only an
|
|
1388
|
+
* explicit `false` disables it, so every config written before this switch
|
|
1389
|
+
* existed — including the pre-region-split flat fields, which never carry
|
|
1390
|
+
* `enabled` — keeps both providers running exactly as before. The card reads the
|
|
1391
|
+
* same rule through `regionEnabledOf`, so the two halves can never disagree
|
|
1392
|
+
* about a region's state.
|
|
1393
|
+
*/
|
|
1394
|
+
declare function regionEnabled(config: Config, region: WorkBuddyRegion): boolean;
|
|
967
1395
|
/**
|
|
968
1396
|
* The account id a region should select, or `undefined` for the documented
|
|
969
1397
|
* default: follow whatever the WorkBuddy app is currently signed in as.
|
|
@@ -1014,4 +1442,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
1014
1442
|
*/
|
|
1015
1443
|
declare function apply(ctx: Context, config: Config): void;
|
|
1016
1444
|
//#endregion
|
|
1017
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyReasoning, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionCleared, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
1445
|
+
export { CREDENTIAL_REJECTED_CODE, Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebSearchPath, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|