@levr-one/auth 1.0.3 → 1.1.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/dist/oauth-client.d.ts.map +1 -1
- package/dist/oauth-client.js +7 -2
- package/dist/oauth-client.js.map +1 -1
- package/dist/oauth-client.test.js +4 -1
- package/dist/oauth-client.test.js.map +1 -1
- package/dist/token-store.d.ts +21 -0
- package/dist/token-store.d.ts.map +1 -1
- package/dist/token-store.js +35 -6
- package/dist/token-store.js.map +1 -1
- package/dist/workspace-store.d.ts +70 -26
- package/dist/workspace-store.d.ts.map +1 -1
- package/dist/workspace-store.js +302 -78
- package/dist/workspace-store.js.map +1 -1
- package/dist/workspace-store.test.js +418 -0
- package/dist/workspace-store.test.js.map +1 -1
- package/package.json +1 -1
package/dist/workspace-store.js
CHANGED
|
@@ -1,20 +1,216 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Disk-backed workspace state at ~/.tq/workspace.json
|
|
3
|
-
*
|
|
2
|
+
* Disk-backed workspace/identity state at ~/.tq/workspace.json
|
|
3
|
+
*
|
|
4
|
+
* Stored as a map keyed by backend URL — the SAME key space as
|
|
5
|
+
* ~/.tq/oauth-tokens.json (see token-store.ts):
|
|
6
|
+
* {
|
|
7
|
+
* "https://api.levr.now": { workspace_id, user_id, user_email, ... },
|
|
8
|
+
* "http://localhost:8180": { ... }
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* internal: this file used to hold a SINGLE global record with no note of
|
|
12
|
+
* which backend it came from. A login or `select_workspace` against a local
|
|
13
|
+
* backend (including a worktree's own backend on its assigned port)
|
|
14
|
+
* overwrote it machine-globally, and the next `yarn setup:mcp` stamped that
|
|
15
|
+
* local workspace id into the `Workspace-Id` header of a STAGING levr URL.
|
|
16
|
+
* Keying by backend URL makes that structurally impossible: a local id lives
|
|
17
|
+
* under a different key than the staging one, so it cannot be read back for
|
|
18
|
+
* a staging endpoint.
|
|
19
|
+
*
|
|
20
|
+
* Backward-compatible: a legacy flat file (`{ workspace_id, ... }` at the top
|
|
21
|
+
* level) is attributed to the currently-configured backend and migrated
|
|
22
|
+
* in-place on first read — the same treatment `readTokenMap()` gives legacy
|
|
23
|
+
* token files. Attribution is a best guess by construction; that is
|
|
24
|
+
* acceptable because this file is a hint-only cache (see `IdentityCache`) and
|
|
25
|
+
* a wrong guess costs one workspace re-selection, not a security decision.
|
|
26
|
+
*
|
|
27
|
+
* Follows the same atomic-write pattern as token-store.ts.
|
|
4
28
|
*/
|
|
5
29
|
import * as fs from 'fs';
|
|
6
30
|
import * as path from 'path';
|
|
7
|
-
import { getTqDir } from './token-store.js';
|
|
31
|
+
import { getTqDir, normalizeBackendUrl } from './token-store.js';
|
|
32
|
+
import { loadConfig } from './config.js';
|
|
8
33
|
const WORKSPACE_FILE = 'workspace.json';
|
|
9
34
|
function getWorkspacePath() {
|
|
10
35
|
return path.join(getTqDir(), WORKSPACE_FILE);
|
|
11
36
|
}
|
|
37
|
+
/** The only fields an identity record — or the mirror — may carry. */
|
|
38
|
+
const IDENTITY_FIELDS = [
|
|
39
|
+
'workspace_id',
|
|
40
|
+
'user_id',
|
|
41
|
+
'user_email',
|
|
42
|
+
'user_name',
|
|
43
|
+
'role',
|
|
44
|
+
];
|
|
45
|
+
function isIdentityKey(key) {
|
|
46
|
+
return /^https?:\/\//i.test(key);
|
|
47
|
+
}
|
|
12
48
|
/**
|
|
13
|
-
*
|
|
49
|
+
* Strip trailing slashes and reject anything that is not an http(s) origin.
|
|
50
|
+
*
|
|
51
|
+
* This is the SINGLE gate both sides use: `resolveKey` will not write under a
|
|
52
|
+
* key that `readIdentityMap` would later discard. Without that agreement a
|
|
53
|
+
* scheme-less `apiBaseUrl` (`localhost:8080`) wrote an entry to disk that
|
|
54
|
+
* every subsequent read dropped — a silent, permanent loss of the pin with no
|
|
55
|
+
* error anywhere.
|
|
56
|
+
*/
|
|
57
|
+
function normalizeKey(url) {
|
|
58
|
+
// Canonical form comes from token-store, so the two files cannot disagree
|
|
59
|
+
// about what key a given backend has (internal). The http(s) gate on top is
|
|
60
|
+
// this store's own: it will not write a key that readIdentityMap discards.
|
|
61
|
+
const trimmed = normalizeBackendUrl(url);
|
|
62
|
+
if (!trimmed || !isIdentityKey(trimmed))
|
|
63
|
+
return undefined;
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The backend the UNKEYED callers actually talk to.
|
|
68
|
+
*
|
|
69
|
+
* This MUST be `loadConfig()`, not `getCurrentApiUrl()`. The two disagree:
|
|
70
|
+
* `getCurrentApiUrl()` has two tiers (`TQ_BACKEND_URL` > `~/.tq/config.json`)
|
|
71
|
+
* while `loadConfig()` has four, and its third is the STORED TOKEN's
|
|
72
|
+
* `apiBaseUrl`. With no `config.json` but a token for a local backend, the
|
|
73
|
+
* two-tier version returns undefined and the old fallback guessed `staging` —
|
|
74
|
+
* filing a LOCAL workspace id under the STAGING key, which `setup-mcp` then
|
|
75
|
+
* stamped into the staging `Workspace-Id` header. That is internal reappearing
|
|
76
|
+
* through its own fix.
|
|
77
|
+
*
|
|
78
|
+
* Using the same resolver the callers use makes the key correct by
|
|
79
|
+
* construction: whatever backend they are about to talk to is the backend the
|
|
80
|
+
* record is filed under, including when that resolution itself ends at the
|
|
81
|
+
* staging default.
|
|
82
|
+
*/
|
|
83
|
+
function configuredBackend() {
|
|
84
|
+
return normalizeKey(loadConfig().apiUrl) ?? 'https://api.levr.now';
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve which backend URL an identity read/write belongs to.
|
|
88
|
+
*
|
|
89
|
+
* An explicit argument (the caller knows its backend) wins; otherwise the
|
|
90
|
+
* configured backend. Callers that know their backend SHOULD pass it.
|
|
91
|
+
* `setup-mcp.ts` in particular must pass the URL it is about to write into
|
|
92
|
+
* `.mcp.json`, which is not necessarily the configured one — the levr MCP
|
|
93
|
+
* host differs from the API host (`https://ai.levr.now/api/v1/mcp` is served
|
|
94
|
+
* by the backend at `https://api.levr.now`), and stdio MCP servers resolve
|
|
95
|
+
* their own backend independently of whatever the levr HTTP entry points at.
|
|
96
|
+
*
|
|
97
|
+
* An explicit-but-malformed key is ignored rather than written, with a log —
|
|
98
|
+
* silently retargeting it at another backend would be the very leak this
|
|
99
|
+
* module exists to prevent.
|
|
100
|
+
*/
|
|
101
|
+
function resolveKey(apiBaseUrl) {
|
|
102
|
+
if (apiBaseUrl !== undefined) {
|
|
103
|
+
const explicit = normalizeKey(apiBaseUrl);
|
|
104
|
+
if (explicit)
|
|
105
|
+
return explicit;
|
|
106
|
+
console.error(`[levr-auth] Ignoring malformed backend url "${apiBaseUrl}" — ` +
|
|
107
|
+
'expected an http(s) origin. Falling back to the configured backend.');
|
|
108
|
+
}
|
|
109
|
+
return configuredBackend();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read the raw identity map from disk.
|
|
113
|
+
* Handles migration from the legacy single-record format.
|
|
114
|
+
*/
|
|
115
|
+
function readIdentityMap() {
|
|
116
|
+
try {
|
|
117
|
+
const filePath = getWorkspacePath();
|
|
118
|
+
if (!fs.existsSync(filePath))
|
|
119
|
+
return {};
|
|
120
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
const parsed = JSON.parse(content);
|
|
122
|
+
// `typeof [] === 'object'` — explicitly reject arrays so property access
|
|
123
|
+
// on a parsed JSON array (`[0]`, `[1]`...) cannot accidentally satisfy
|
|
124
|
+
// the per-field checks in loadIdentityCache().
|
|
125
|
+
if (parsed === null ||
|
|
126
|
+
typeof parsed !== 'object' ||
|
|
127
|
+
Array.isArray(parsed)) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
// Keep only URL-shaped keys. This also strips the back-compat mirror
|
|
131
|
+
// fields (see writeIdentityMap) so they can never be mistaken for a
|
|
132
|
+
// backend entry.
|
|
133
|
+
const map = {};
|
|
134
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
135
|
+
if (!isIdentityKey(key))
|
|
136
|
+
continue;
|
|
137
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Normalize on read, mirroring readTokenMap: a `.../` entry and a `...`
|
|
141
|
+
// entry are the same backend, and only the canonical form is ever
|
|
142
|
+
// looked up, so an un-normalized key would be permanently unreachable.
|
|
143
|
+
map[normalizeKey(key) ?? key] = value;
|
|
144
|
+
}
|
|
145
|
+
// Legacy flat shape — `{ workspace_id, ... }` and NOT ONE url key. The
|
|
146
|
+
// test is the ABSENCE of url keys, never the presence of `workspace_id`:
|
|
147
|
+
// a current file carries BOTH (the mirror), and treating that as legacy
|
|
148
|
+
// would collapse every backend's entry into one on the next read.
|
|
149
|
+
if (Object.keys(map).length === 0 && 'workspace_id' in parsed) {
|
|
150
|
+
const key = configuredBackend();
|
|
151
|
+
const migrated = { [key]: parsed };
|
|
152
|
+
writeIdentityMap(migrated);
|
|
153
|
+
return migrated;
|
|
154
|
+
}
|
|
155
|
+
return map;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error('[levr-auth] Failed to load workspace:', error);
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Atomically write the identity map to `~/.tq/workspace.json`.
|
|
14
164
|
* Ensures the ~/.tq directory exists with 0o700, writes to `*.tmp` with
|
|
15
165
|
* 0o600, then renames over the target. Errors are logged, never thrown.
|
|
166
|
+
*
|
|
167
|
+
* ## TRANSITIONAL: the top-level mirror
|
|
168
|
+
*
|
|
169
|
+
* Alongside the url-keyed entries we also write the CONFIGURED backend's
|
|
170
|
+
* record at the top level, in the pre-internal flat shape:
|
|
171
|
+
*
|
|
172
|
+
* {
|
|
173
|
+
* "https://api.levr.now": { workspace_id: "A", ... },
|
|
174
|
+
* "http://localhost:8180": { workspace_id: "B", ... },
|
|
175
|
+
* "workspace_id": "A", "user_id": ..., ... <-- mirror of the configured one
|
|
176
|
+
* }
|
|
177
|
+
*
|
|
178
|
+
* ~/.tq is machine-global but every checkout runs its OWN
|
|
179
|
+
* `plugins/<name>/dist/index.js` (the `.mcp.json` entries use relative
|
|
180
|
+
* paths, so each worktree loads its own build), and
|
|
181
|
+
* a worktree on a feature branch does not get this code until that branch
|
|
182
|
+
* merges main — days or weeks. Without the mirror, an old stdio server in
|
|
183
|
+
* such a worktree reads `.workspace_id`, gets undefined, falls through to
|
|
184
|
+
* `ensureWorkspace()`, and its old `saveWorkspace()` REWRITES THE WHOLE FILE
|
|
185
|
+
* FLAT — wiping every other backend's entry. Main then re-migrates it, and
|
|
186
|
+
* the two ping-pong for as long as any stale checkout is in use.
|
|
187
|
+
*
|
|
188
|
+
* The mirror keeps those old readers resolving, so they never reach the
|
|
189
|
+
* read-fallthrough that ends in a flattening write.
|
|
190
|
+
*
|
|
191
|
+
* It does NOT stop an old WRITER, and that is not only the deliberate case
|
|
192
|
+
* (`tq:workspace` in a stale checkout). Observed live: `yarn tq:login`
|
|
193
|
+
* rewrites oauth-tokens.json, the running qinetic-mcp server's token watcher
|
|
194
|
+
* fires, and `qinetic-mcp/src/auth.ts` calls `saveIdentityCache()`
|
|
195
|
+
* unconditionally after re-auth — flattening the file two seconds after the
|
|
196
|
+
* login wrote it, with no user action at all.
|
|
197
|
+
*
|
|
198
|
+
* Note what decides that: a long-lived process holds whatever `@levr-one/auth`
|
|
199
|
+
* it imported AT STARTUP, so a freshly rebuilt `dist/` on disk changes
|
|
200
|
+
* nothing until the server is restarted (`/mcp` reconnect). Checkout
|
|
201
|
+
* freshness is necessary but not sufficient.
|
|
202
|
+
*
|
|
203
|
+
* This is survivable rather than fixed: the flat file still carries the
|
|
204
|
+
* correct id for the configured backend, and the next read by new code
|
|
205
|
+
* re-migrates it. What is lost each time is OTHER backends' entries — local
|
|
206
|
+
* worktree pins, which re-derive on their own since those backends
|
|
207
|
+
* auto-select their single seeded workspace. Nothing new code writes can
|
|
208
|
+
* prevent an old writer from overwriting the file.
|
|
209
|
+
*
|
|
210
|
+
* REMOVE once every checkout has rebuilt against this version — the mirror is
|
|
211
|
+
* only meaningful for one backend and is pure carrying cost after that.
|
|
16
212
|
*/
|
|
17
|
-
function
|
|
213
|
+
function writeIdentityMap(map) {
|
|
18
214
|
try {
|
|
19
215
|
const tqDir = getTqDir();
|
|
20
216
|
if (!fs.existsSync(tqDir)) {
|
|
@@ -22,6 +218,19 @@ function writeWorkspaceJsonAtomic(payload) {
|
|
|
22
218
|
}
|
|
23
219
|
const filePath = getWorkspacePath();
|
|
24
220
|
const tempPath = filePath + '.tmp';
|
|
221
|
+
// Mirror ONLY the five IdentityCache fields. Spreading the entry wholesale
|
|
222
|
+
// promoted any url-shaped field INSIDE it to a top-level entry, where the
|
|
223
|
+
// next read would serve it as that backend's record — a hand-edited or
|
|
224
|
+
// corrupted file could put an arbitrary workspace id under an arbitrary
|
|
225
|
+
// backend.
|
|
226
|
+
const mirrored = map[configuredBackend()];
|
|
227
|
+
const payload = { ...map };
|
|
228
|
+
if (mirrored) {
|
|
229
|
+
for (const field of IDENTITY_FIELDS) {
|
|
230
|
+
if (typeof mirrored[field] === 'string')
|
|
231
|
+
payload[field] = mirrored[field];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
25
234
|
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), {
|
|
26
235
|
mode: 0o600,
|
|
27
236
|
});
|
|
@@ -32,112 +241,127 @@ function writeWorkspaceJsonAtomic(payload) {
|
|
|
32
241
|
}
|
|
33
242
|
}
|
|
34
243
|
/**
|
|
35
|
-
* Load persisted workspace ID
|
|
36
|
-
*
|
|
244
|
+
* Load the persisted workspace ID for a backend.
|
|
245
|
+
*
|
|
246
|
+
* @param apiBaseUrl Backend the id must belong to. Defaults to the currently
|
|
247
|
+
* configured backend — correct for stdio MCP servers and the CLI, which
|
|
248
|
+
* talk to whatever `~/.tq/config.json` points at.
|
|
249
|
+
* @returns workspace_id, or null when this backend has no entry
|
|
37
250
|
*/
|
|
38
|
-
export function loadWorkspace() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
43
|
-
const data = JSON.parse(content);
|
|
44
|
-
if (data.workspace_id && typeof data.workspace_id === 'string') {
|
|
45
|
-
return data.workspace_id;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
console.error('[levr-auth] Failed to load workspace:', error);
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
251
|
+
export function loadWorkspace(apiBaseUrl) {
|
|
252
|
+
const entry = readIdentityMap()[resolveKey(apiBaseUrl)];
|
|
253
|
+
const id = entry?.workspace_id;
|
|
254
|
+
return typeof id === 'string' && id ? id : null;
|
|
53
255
|
}
|
|
54
256
|
/**
|
|
55
|
-
* Save workspace ID
|
|
257
|
+
* Save a workspace ID against a backend, leaving every other backend's entry
|
|
258
|
+
* intact. Preserves any identity fields already cached for that backend.
|
|
56
259
|
*/
|
|
57
|
-
export function saveWorkspace(workspaceId) {
|
|
58
|
-
|
|
260
|
+
export function saveWorkspace(workspaceId, apiBaseUrl) {
|
|
261
|
+
const map = readIdentityMap();
|
|
262
|
+
const key = resolveKey(apiBaseUrl);
|
|
263
|
+
map[key] = { ...map[key], workspace_id: workspaceId };
|
|
264
|
+
writeIdentityMap(map);
|
|
59
265
|
}
|
|
60
266
|
/**
|
|
61
|
-
* Load the full identity cache
|
|
267
|
+
* Load the full identity cache for a backend.
|
|
62
268
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* - legacy single-field shape (`{ workspace_id }` only)
|
|
68
|
-
* - any field present but not a string
|
|
269
|
+
* Returns a typed `IdentityCache` only when all five fields are present and
|
|
270
|
+
* are strings. Returns `null` for a missing file, malformed JSON, a backend
|
|
271
|
+
* with no entry, a workspace-id-only entry, or any field that is not a
|
|
272
|
+
* string.
|
|
69
273
|
*
|
|
70
274
|
* Never throws — graceful degradation is mandatory because callers wire this
|
|
71
275
|
* into startup paths and watcher callbacks.
|
|
72
276
|
*/
|
|
73
|
-
export function loadIdentityCache() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!fs.existsSync(filePath))
|
|
77
|
-
return null;
|
|
78
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
79
|
-
const data = JSON.parse(content);
|
|
80
|
-
// `typeof [] === 'object'` — explicitly reject arrays so property
|
|
81
|
-
// access on a parsed JSON array (`[0]`, `[1]`...) cannot accidentally
|
|
82
|
-
// satisfy the per-field checks below.
|
|
83
|
-
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
const keys = [
|
|
87
|
-
'workspace_id',
|
|
88
|
-
'user_id',
|
|
89
|
-
'user_email',
|
|
90
|
-
'user_name',
|
|
91
|
-
'role',
|
|
92
|
-
];
|
|
93
|
-
for (const key of keys) {
|
|
94
|
-
const value = data[key];
|
|
95
|
-
if (typeof value !== 'string')
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
workspace_id: data.workspace_id,
|
|
100
|
-
user_id: data.user_id,
|
|
101
|
-
user_email: data.user_email,
|
|
102
|
-
user_name: data.user_name,
|
|
103
|
-
role: data.role,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
277
|
+
export function loadIdentityCache(apiBaseUrl) {
|
|
278
|
+
const entry = readIdentityMap()[resolveKey(apiBaseUrl)];
|
|
279
|
+
if (!entry)
|
|
107
280
|
return null;
|
|
281
|
+
const keys = [
|
|
282
|
+
'workspace_id',
|
|
283
|
+
'user_id',
|
|
284
|
+
'user_email',
|
|
285
|
+
'user_name',
|
|
286
|
+
'role',
|
|
287
|
+
];
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
if (typeof entry[key] !== 'string')
|
|
290
|
+
return null;
|
|
108
291
|
}
|
|
292
|
+
return {
|
|
293
|
+
workspace_id: entry.workspace_id,
|
|
294
|
+
user_id: entry.user_id,
|
|
295
|
+
user_email: entry.user_email,
|
|
296
|
+
user_name: entry.user_name,
|
|
297
|
+
role: entry.role,
|
|
298
|
+
};
|
|
109
299
|
}
|
|
110
300
|
/**
|
|
111
|
-
* Save the full identity cache
|
|
112
|
-
*
|
|
113
|
-
* Single writer pattern: `saveWorkspace` writes only `{ workspace_id }`;
|
|
114
|
-
* `saveIdentityCache` is the only path that writes the five-field shape.
|
|
115
|
-
* After the first identity-aware save, the file always contains all five
|
|
116
|
-
* fields. Same `.tmp` + `renameSync` pattern as `saveWorkspace`.
|
|
301
|
+
* Save the full identity cache for a backend with an atomic write, leaving
|
|
302
|
+
* every other backend's entry intact.
|
|
117
303
|
*/
|
|
118
|
-
export function saveIdentityCache(data) {
|
|
119
|
-
|
|
304
|
+
export function saveIdentityCache(data, apiBaseUrl) {
|
|
305
|
+
const map = readIdentityMap();
|
|
306
|
+
map[resolveKey(apiBaseUrl)] = {
|
|
120
307
|
workspace_id: data.workspace_id,
|
|
121
308
|
user_id: data.user_id,
|
|
122
309
|
user_email: data.user_email,
|
|
123
310
|
user_name: data.user_name,
|
|
124
311
|
role: data.role,
|
|
125
|
-
}
|
|
312
|
+
};
|
|
313
|
+
writeIdentityMap(map);
|
|
126
314
|
}
|
|
127
315
|
/**
|
|
128
|
-
* Clear persisted
|
|
316
|
+
* Clear the persisted identity for ONE backend (e.g. on logout).
|
|
317
|
+
*
|
|
318
|
+
* Mirrors `clearTokens()`: only the current backend's entry is removed, and
|
|
319
|
+
* the file is deleted outright once the last entry is gone. An env switch
|
|
320
|
+
* must NOT call this — under URL keying, switching environments already
|
|
321
|
+
* selects a different entry, and clearing would throw away a still-valid
|
|
322
|
+
* workspace pin the user would have to re-select.
|
|
129
323
|
*/
|
|
130
|
-
export function clearWorkspace() {
|
|
324
|
+
export function clearWorkspace(apiBaseUrl) {
|
|
131
325
|
try {
|
|
132
326
|
const filePath = getWorkspacePath();
|
|
133
|
-
if (fs.existsSync(filePath))
|
|
327
|
+
if (!fs.existsSync(filePath))
|
|
328
|
+
return;
|
|
329
|
+
const map = readIdentityMap();
|
|
330
|
+
const key = resolveKey(apiBaseUrl);
|
|
331
|
+
if (!(key in map))
|
|
332
|
+
return;
|
|
333
|
+
delete map[key];
|
|
334
|
+
if (Object.keys(map).length === 0) {
|
|
134
335
|
fs.unlinkSync(filePath);
|
|
135
336
|
}
|
|
337
|
+
else {
|
|
338
|
+
writeIdentityMap(map);
|
|
339
|
+
}
|
|
136
340
|
}
|
|
137
341
|
catch (error) {
|
|
138
342
|
console.error('[levr-auth] Failed to clear workspace:', error);
|
|
139
343
|
}
|
|
140
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Re-point the transitional top-level mirror at the CURRENTLY configured
|
|
347
|
+
* backend, without touching any keyed entry. Call this after changing which
|
|
348
|
+
* backend is configured (`tq:env`).
|
|
349
|
+
*
|
|
350
|
+
* The mirror is only recomputed on write, and an env switch writes nothing —
|
|
351
|
+
* so without this the mirror keeps pointing at the PREVIOUS backend and an
|
|
352
|
+
* old (pre-internal) client reads a pin for a backend it is no longer talking
|
|
353
|
+
* to. That is strictly worse than what it replaced: before the file was
|
|
354
|
+
* keyed, `tq:env` deleted the whole file, so an old client got nothing and
|
|
355
|
+
* fell back to unpinned. Failing to unpinned is safe; failing to another
|
|
356
|
+
* backend's workspace is not.
|
|
357
|
+
*
|
|
358
|
+
* No-op when the file does not exist. Remove alongside the mirror itself.
|
|
359
|
+
*/
|
|
360
|
+
export function refreshMirror() {
|
|
361
|
+
if (!fs.existsSync(getWorkspacePath()))
|
|
362
|
+
return;
|
|
363
|
+
writeIdentityMap(readIdentityMap());
|
|
364
|
+
}
|
|
141
365
|
/**
|
|
142
366
|
* Get workspace file path (for debugging)
|
|
143
367
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-store.js","sourceRoot":"","sources":["../src/workspace-store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"workspace-store.js","sourceRoot":"","sources":["../src/workspace-store.ts"],"names":[],"mappings":"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"}
|