zcode-acp-server 0.46.7 → 0.47.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/README.md +1 -1
- package/dist/code-fingerprint.json +2 -2
- package/dist/remote/endpoint.d.ts.map +1 -1
- package/dist/remote/endpoint.js +23 -0
- package/dist/remote/endpoint.js.map +1 -1
- package/dist/remote/hub-server.d.ts.map +1 -1
- package/dist/remote/hub-server.js +115 -2
- package/dist/remote/hub-server.js.map +1 -1
- package/dist/remote/settings-endpoint.d.ts +47 -0
- package/dist/remote/settings-endpoint.d.ts.map +1 -0
- package/dist/remote/settings-endpoint.js +1046 -0
- package/dist/remote/settings-endpoint.js.map +1 -0
- package/dist/settings/agents-config.d.ts +158 -0
- package/dist/settings/agents-config.d.ts.map +1 -0
- package/dist/settings/agents-config.js +598 -0
- package/dist/settings/agents-config.js.map +1 -0
- package/dist/settings/app-update.d.ts +193 -0
- package/dist/settings/app-update.d.ts.map +1 -0
- package/dist/settings/app-update.js +659 -0
- package/dist/settings/app-update.js.map +1 -0
- package/dist/settings/atomic-write.d.ts +106 -0
- package/dist/settings/atomic-write.d.ts.map +1 -0
- package/dist/settings/atomic-write.js +228 -0
- package/dist/settings/atomic-write.js.map +1 -0
- package/dist/settings/cli-config.d.ts +162 -0
- package/dist/settings/cli-config.d.ts.map +1 -0
- package/dist/settings/cli-config.js +369 -0
- package/dist/settings/cli-config.js.map +1 -0
- package/dist/settings/coding-plan.d.ts +106 -0
- package/dist/settings/coding-plan.d.ts.map +1 -0
- package/dist/settings/coding-plan.js +299 -0
- package/dist/settings/coding-plan.js.map +1 -0
- package/dist/settings/file-lock.d.ts +39 -0
- package/dist/settings/file-lock.d.ts.map +1 -0
- package/dist/settings/file-lock.js +282 -0
- package/dist/settings/file-lock.js.map +1 -0
- package/dist/settings/provider-config.d.ts +123 -0
- package/dist/settings/provider-config.d.ts.map +1 -0
- package/dist/settings/provider-config.js +311 -0
- package/dist/settings/provider-config.js.map +1 -0
- package/dist/settings/skills.d.ts +72 -0
- package/dist/settings/skills.d.ts.map +1 -0
- package/dist/settings/skills.js +245 -0
- package/dist/settings/skills.js.map +1 -0
- package/dist/settings/usage-stats.d.ts +74 -0
- package/dist/settings/usage-stats.d.ts.map +1 -0
- package/dist/settings/usage-stats.js +235 -0
- package/dist/settings/usage-stats.js.map +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +22 -0
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +65 -0
- package/docs/REMOTE-CLIENTS.md +285 -0
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-plan quota reset cards.
|
|
3
|
+
*
|
|
4
|
+
* The desktop app exposes "reset cards" — a limited number of quota resets the
|
|
5
|
+
* user can spend to clear a 5-hour or weekly window. This module is the headless
|
|
6
|
+
* equivalent: the same four endpoints, the same headers, the same credentials,
|
|
7
|
+
* so a client that cannot open the app still gets the feature.
|
|
8
|
+
*
|
|
9
|
+
* Credentials are the hard part. They live in `~/.zcode/v2/credentials.json`,
|
|
10
|
+
* AES-256-GCM encrypted, keyed by `ZCODE_CREDENTIAL_SECRET` or a machine-bound
|
|
11
|
+
* fallback string. Two tokens are needed and they are NOT interchangeable:
|
|
12
|
+
*
|
|
13
|
+
* - `zcodejwttoken` → `Authorization: Bearer …`
|
|
14
|
+
* - the OAuth access token for the CURRENT provider family → `X-Bigmodel-Authorization`
|
|
15
|
+
*
|
|
16
|
+
* Picking the family correctly matters: a `zai`-only account has no bigmodel
|
|
17
|
+
* token, and falling back across families would send a token the backend
|
|
18
|
+
* rejects. The family is read from the provider id (`account:zai-…` vs
|
|
19
|
+
* `account:bigmodel-…`), which is the same spelling the registry uses.
|
|
20
|
+
*
|
|
21
|
+
* Known limit: a TEAM plan's requests need `Bigmodel-Organization` /
|
|
22
|
+
* `Bigmodel-Project` headers, and the ids are not in the credential store a
|
|
23
|
+
* headless process can read. Team-plan resets therefore answer a backend error
|
|
24
|
+
* rather than working; personal plans (the common case) are unaffected.
|
|
25
|
+
*
|
|
26
|
+
* This is the ONLY irreversible operation in the settings API. A spent card is
|
|
27
|
+
* gone. `use()` therefore takes an idempotency key so a retry (a dropped
|
|
28
|
+
* response, a double tap) cannot burn two cards, and the route layer requires a
|
|
29
|
+
* fresh status nonce before it will call it.
|
|
30
|
+
*/
|
|
31
|
+
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
32
|
+
import { readFile } from "node:fs/promises";
|
|
33
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
34
|
+
import { log, zcodeCredentialsPath } from "../utils.js";
|
|
35
|
+
/** The only provider ids that own a coding plan. */
|
|
36
|
+
const CODING_PLAN_PROVIDER_PREFIX = "account:";
|
|
37
|
+
const ENCRYPTED_PREFIX = "enc:v1:";
|
|
38
|
+
const CIPHER_ALGORITHM = "aes-256-gcm";
|
|
39
|
+
const ZCODE_JWT_KEY = "zcodejwttoken";
|
|
40
|
+
const OAUTH_TOKEN_KEYS = {
|
|
41
|
+
bigmodel: "oauth:bigmodel:access_token",
|
|
42
|
+
zai: "oauth:zai:access_token",
|
|
43
|
+
};
|
|
44
|
+
/** The API origin; overridable for testing and for a self-hosted gateway. */
|
|
45
|
+
function apiOrigin(env = process.env) {
|
|
46
|
+
return env.ZCODE_ENDPOINT_ORIGIN ?? env.ZCODE_BASE_URL ?? "https://zcode.z.ai";
|
|
47
|
+
}
|
|
48
|
+
const RESET_BASE_PATH = "/api/v1/coding-plan/reset";
|
|
49
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
50
|
+
/** Machine-bound fallback secret, mirroring ZCode's credential cipher. */
|
|
51
|
+
function resolveCredentialSecret(env) {
|
|
52
|
+
const configured = env.ZCODE_CREDENTIAL_SECRET?.trim();
|
|
53
|
+
if (configured)
|
|
54
|
+
return configured;
|
|
55
|
+
let username = "unknown";
|
|
56
|
+
try {
|
|
57
|
+
username = userInfo().username;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* sandboxed runtimes may not resolve it */
|
|
61
|
+
}
|
|
62
|
+
return `zcode-credential-fallback:${platform()}:${homedir()}:${username}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decrypt one `enc:v1:` credential value.
|
|
66
|
+
*
|
|
67
|
+
* The wire form is `enc:v1:<iv>.<authTag>.<ciphertext>`, all base64url. A value
|
|
68
|
+
* without the prefix is returned as-is (plaintext entries exist in the wild).
|
|
69
|
+
*/
|
|
70
|
+
function decryptCredential(value, secret) {
|
|
71
|
+
if (!value.startsWith(ENCRYPTED_PREFIX))
|
|
72
|
+
return value;
|
|
73
|
+
const payload = value.slice(ENCRYPTED_PREFIX.length);
|
|
74
|
+
const parts = payload.split(".");
|
|
75
|
+
if (parts.length !== 3)
|
|
76
|
+
throw new Error("credential_decrypt_malformed");
|
|
77
|
+
const [ivRaw, tagRaw, cipherRaw] = parts;
|
|
78
|
+
const iv = Buffer.from(ivRaw, "base64url");
|
|
79
|
+
const authTag = Buffer.from(tagRaw, "base64url");
|
|
80
|
+
const cipherText = Buffer.from(cipherRaw, "base64url");
|
|
81
|
+
const key = createHash("sha256").update(secret).digest();
|
|
82
|
+
try {
|
|
83
|
+
const decipher = createDecipheriv(CIPHER_ALGORITHM, key, iv);
|
|
84
|
+
decipher.setAuthTag(authTag);
|
|
85
|
+
return Buffer.concat([decipher.update(cipherText), decipher.final()]).toString("utf8");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error("credentials_unavailable");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Read and decrypt the credential store. Throws `credentials_unavailable`. */
|
|
92
|
+
async function loadCredentials(env) {
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
raw = await readFile(zcodeCredentialsPath(), "utf8");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error("credentials_unavailable");
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(raw);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error("credentials_unavailable");
|
|
106
|
+
}
|
|
107
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
108
|
+
throw new Error("credentials_unavailable");
|
|
109
|
+
}
|
|
110
|
+
const secret = resolveCredentialSecret(env);
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
113
|
+
if (typeof value !== "string")
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
out[key] = decryptCredential(value, secret);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// One undecryptable entry must not sink the rest — the store holds OAuth
|
|
120
|
+
// discovery state and other unrelated secrets.
|
|
121
|
+
log(`coding-plan: credential '${key}' could not be read (${error instanceof Error ? error.message : String(error)})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Which family a provider id belongs to. */
|
|
127
|
+
export function resetFamilyFor(providerId) {
|
|
128
|
+
return providerId.includes(":zai-") ? "zai" : "bigmodel";
|
|
129
|
+
}
|
|
130
|
+
/** True when the provider id can own a coding plan. */
|
|
131
|
+
export function isCodingPlanProvider(providerId) {
|
|
132
|
+
if (!providerId.startsWith(CODING_PLAN_PROVIDER_PREFIX))
|
|
133
|
+
return false;
|
|
134
|
+
return /^account:(zai|bigmodel)-(individual|team)-coding-plan$/u.test(providerId);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve both tokens for a reset request.
|
|
138
|
+
*
|
|
139
|
+
* @throws `coding_plan_provider_required` for a non-account provider,
|
|
140
|
+
* `credentials_unavailable` when the store cannot be decrypted, and
|
|
141
|
+
* `coding_plan_<x>_jwt_required` when a token is missing.
|
|
142
|
+
*/
|
|
143
|
+
export async function resolveResetAuthorization(providerId, env = process.env) {
|
|
144
|
+
if (!isCodingPlanProvider(providerId)) {
|
|
145
|
+
throw new Error("coding_plan_provider_required");
|
|
146
|
+
}
|
|
147
|
+
const credentials = await loadCredentials(env);
|
|
148
|
+
const zcodeJwt = credentials[ZCODE_JWT_KEY]?.trim();
|
|
149
|
+
if (!zcodeJwt)
|
|
150
|
+
throw new Error("coding_plan_zcode_jwt_required");
|
|
151
|
+
const family = resetFamilyFor(providerId);
|
|
152
|
+
const codingPlanJwt = credentials[OAUTH_TOKEN_KEYS[family]]?.trim();
|
|
153
|
+
if (!codingPlanJwt)
|
|
154
|
+
throw new Error("coding_plan_maas_jwt_required");
|
|
155
|
+
return {
|
|
156
|
+
zcodeAuthorization: /^Bearer\s/iu.test(zcodeJwt) ? zcodeJwt : `Bearer ${zcodeJwt}`,
|
|
157
|
+
codingPlanAuthorization: codingPlanJwt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function resetHeaders(auth, includeScope) {
|
|
161
|
+
const headers = {
|
|
162
|
+
Authorization: auth.zcodeAuthorization,
|
|
163
|
+
// The contract sends the MaaS JWT verbatim — no Bearer prefix.
|
|
164
|
+
"X-Bigmodel-Authorization": auth.codingPlanAuthorization,
|
|
165
|
+
};
|
|
166
|
+
if (!includeScope)
|
|
167
|
+
return headers;
|
|
168
|
+
headers["Bigmodel-Target-Type"] = auth.teamContext ? "TEAM" : "PERSONAL";
|
|
169
|
+
if (auth.teamContext) {
|
|
170
|
+
headers["Bigmodel-Organization"] = auth.teamContext.organizationId;
|
|
171
|
+
headers["Bigmodel-Project"] = auth.teamContext.projectId;
|
|
172
|
+
}
|
|
173
|
+
return headers;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read one reset endpoint, honouring its envelope contract.
|
|
177
|
+
*
|
|
178
|
+
* The backend reports business failures as `{code}` even on HTTP 4xx, so the
|
|
179
|
+
* code — never the message — decides success. Codes the caller expects are
|
|
180
|
+
* passed in so a "denied" answer can be surfaced as data instead of a throw.
|
|
181
|
+
*/
|
|
182
|
+
async function callReset(path, init, auth, acceptedCodes = [], fetchImpl = globalThis.fetch, env = process.env) {
|
|
183
|
+
const response = await fetchImpl(`${apiOrigin(env)}${path}`, {
|
|
184
|
+
method: init.method,
|
|
185
|
+
headers: {
|
|
186
|
+
...resetHeaders(auth, init.includeScope),
|
|
187
|
+
...(init.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
188
|
+
},
|
|
189
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
190
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
191
|
+
});
|
|
192
|
+
let payload;
|
|
193
|
+
try {
|
|
194
|
+
payload = JSON.parse(await response.text());
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
payload = null;
|
|
198
|
+
}
|
|
199
|
+
const envelope = (payload ?? {});
|
|
200
|
+
if (typeof envelope.code === "number" && envelope.code !== 0) {
|
|
201
|
+
if (acceptedCodes.includes(envelope.code))
|
|
202
|
+
return envelope;
|
|
203
|
+
throw new Error(`coding_plan_reset_api_error:${envelope.code}`);
|
|
204
|
+
}
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new Error(`coding_plan_reset_http_error:${response.status}`);
|
|
207
|
+
}
|
|
208
|
+
return envelope;
|
|
209
|
+
}
|
|
210
|
+
/** 3301 is the backend's "not now" answer for an opportunity request. */
|
|
211
|
+
const OPPORTUNITY_DENIED_CODE = 3301;
|
|
212
|
+
/** Fetch the current card inventory. */
|
|
213
|
+
export async function readResetStatus(providerId, fetchImpl = globalThis.fetch, env = process.env) {
|
|
214
|
+
const auth = await resolveResetAuthorization(providerId, env);
|
|
215
|
+
const envelope = (await callReset(`${RESET_BASE_PATH}/status`, { method: "GET", includeScope: true }, auth, [], fetchImpl, env));
|
|
216
|
+
const data = envelope.data ?? {};
|
|
217
|
+
const readList = (key) => {
|
|
218
|
+
const value = data[key];
|
|
219
|
+
if (!Array.isArray(value))
|
|
220
|
+
return [];
|
|
221
|
+
return value
|
|
222
|
+
.filter((item) => typeof item === "object" &&
|
|
223
|
+
item !== null &&
|
|
224
|
+
typeof item.expire_at === "number")
|
|
225
|
+
.map((item) => ({ expireAt: item.expire_at }));
|
|
226
|
+
};
|
|
227
|
+
const readHistory = (key) => {
|
|
228
|
+
const value = data[key];
|
|
229
|
+
if (typeof value !== "object" || value === null)
|
|
230
|
+
return null;
|
|
231
|
+
const usedAt = value.used_at;
|
|
232
|
+
return typeof usedAt === "number" ? { usedAt } : null;
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
availableFiveHour: readList("available_five_hour_resets"),
|
|
236
|
+
availableWeek: readList("available_week_resets"),
|
|
237
|
+
latestFiveHour: readHistory("latest_five_hour_reset_history"),
|
|
238
|
+
latestWeek: readHistory("latest_week_reset_history"),
|
|
239
|
+
hasUnreadHistory: data["has_unread_history"] === true,
|
|
240
|
+
nonce: randomUUID(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Spend one card.
|
|
245
|
+
*
|
|
246
|
+
* The idempotency key is what makes a retry safe: the same key always answers
|
|
247
|
+
* the same outcome instead of consuming a second card. The caller supplies it
|
|
248
|
+
* (the route layer derives one per request), so a client that retries can
|
|
249
|
+
* reuse the value it already sent.
|
|
250
|
+
*/
|
|
251
|
+
export async function useResetCard(providerId, resetType, idempotencyKey, fetchImpl = globalThis.fetch, env = process.env) {
|
|
252
|
+
const key = idempotencyKey.trim();
|
|
253
|
+
if (!key || key.length > 64)
|
|
254
|
+
throw new Error("coding_plan_reset_invalid_idempotency_key");
|
|
255
|
+
const auth = await resolveResetAuthorization(providerId, env);
|
|
256
|
+
const envelope = (await callReset(`${RESET_BASE_PATH}/use`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
includeScope: true,
|
|
259
|
+
body: { idempotency_key: key, reset_type: resetType },
|
|
260
|
+
}, auth, [], fetchImpl, env));
|
|
261
|
+
return { used: envelope.data?.used === true };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Ask whether a reset would be granted right now.
|
|
265
|
+
*
|
|
266
|
+
* A denial (code 3301) is a normal answer carrying `next_try_at`, not a
|
|
267
|
+
* failure — the UI shows a countdown rather than an error.
|
|
268
|
+
*/
|
|
269
|
+
export async function requestResetOpportunity(providerId, idempotencyKey, fetchImpl = globalThis.fetch, env = process.env) {
|
|
270
|
+
const key = idempotencyKey.trim();
|
|
271
|
+
if (!key || key.length > 64)
|
|
272
|
+
throw new Error("coding_plan_reset_invalid_idempotency_key");
|
|
273
|
+
const auth = await resolveResetAuthorization(providerId, env);
|
|
274
|
+
const envelope = (await callReset(`${RESET_BASE_PATH}/opportunity`, { method: "POST", includeScope: true, body: { idempotency_key: key } }, auth, [OPPORTUNITY_DENIED_CODE], fetchImpl, env));
|
|
275
|
+
if (envelope.code === OPPORTUNITY_DENIED_CODE) {
|
|
276
|
+
const nextTryAt = envelope.data?.next_try_at;
|
|
277
|
+
return { granted: false, nextTryAt: typeof nextTryAt === "number" ? nextTryAt : null };
|
|
278
|
+
}
|
|
279
|
+
return { granted: envelope.data?.granted === true, nextTryAt: null };
|
|
280
|
+
}
|
|
281
|
+
/** Mark the reset history read (clears the unread badge). */
|
|
282
|
+
export async function markResetHistoryRead(providerId, fetchImpl = globalThis.fetch, env = process.env) {
|
|
283
|
+
const auth = await resolveResetAuthorization(providerId, env);
|
|
284
|
+
// No target scope on this one: it validates identity but shares a read
|
|
285
|
+
// cursor across the account.
|
|
286
|
+
await callReset(`${RESET_BASE_PATH}/history/read`, { method: "POST", includeScope: false }, auth, [], fetchImpl, env);
|
|
287
|
+
}
|
|
288
|
+
/** Exposed for the eligibility check the route layer runs before any spend. */
|
|
289
|
+
export function codingPlanProviderIds() {
|
|
290
|
+
// The registry spelling is the source of truth; these are the ids a coding
|
|
291
|
+
// plan can be attached to, and only they may spend a card.
|
|
292
|
+
return [
|
|
293
|
+
"account:bigmodel-individual-coding-plan",
|
|
294
|
+
"account:bigmodel-team-coding-plan",
|
|
295
|
+
"account:zai-individual-coding-plan",
|
|
296
|
+
"account:zai-team-coding-plan",
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=coding-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coding-plan.js","sourceRoot":"","sources":["../../src/settings/coding-plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process file lock — the same protocol the ZCode desktop app uses.
|
|
3
|
+
*
|
|
4
|
+
* The settings API writes config files the desktop app also writes. An atomic
|
|
5
|
+
* rename makes each individual write safe, but it does NOT make two writers
|
|
6
|
+
* mutually exclusive: a settings write that lands while the app is writing the
|
|
7
|
+
* same file silently discards the app's change. Sharing the app's lock is the
|
|
8
|
+
* only way to avoid that, so this module re-implements its protocol exactly
|
|
9
|
+
* (ADR-0026):
|
|
10
|
+
*
|
|
11
|
+
* - lock directory `<path>.lock`, created with `mkdir` (atomic on POSIX)
|
|
12
|
+
* - one `owner-<pid>-<ts>-<rand>.json` per holder, written `wx`
|
|
13
|
+
* - a holder is authoritative only while its own owner file is the sole
|
|
14
|
+
* entry and the directory identity (dev+ino) has not changed
|
|
15
|
+
* - stale reclaim: owner pid no longer alive, or an ownerless lock past the
|
|
16
|
+
* grace window. Never steal from a live holder.
|
|
17
|
+
*
|
|
18
|
+
* Timestamps in owner files are validated against a clock-skew ceiling so a
|
|
19
|
+
* malformed or future-dated `createdAt` cannot keep a lock permanently
|
|
20
|
+
* unreclaimable.
|
|
21
|
+
*
|
|
22
|
+
* Everything is best-effort in the caller's favour: reaching the wait cap
|
|
23
|
+
* throws a lock-timeout error rather than deleting a newer writer's lock.
|
|
24
|
+
*/
|
|
25
|
+
export interface FileLockOptions {
|
|
26
|
+
lockRetryDelaysMs?: readonly number[];
|
|
27
|
+
lockOwnerlessGraceMs?: number;
|
|
28
|
+
lockMaxWaitMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Error code for a lock that could not be acquired within the wait cap. */
|
|
31
|
+
export declare const FILE_LOCK_TIMEOUT_ERROR_CODE = "ZCODE_FILE_LOCK_TIMEOUT";
|
|
32
|
+
/**
|
|
33
|
+
* Run `operation` while holding ZCode's lock for `filePath`.
|
|
34
|
+
*
|
|
35
|
+
* @throws with code {@link FILE_LOCK_TIMEOUT_ERROR_CODE} when the lock could
|
|
36
|
+
* not be acquired within `lockMaxWaitMs`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function withFileLock<T>(filePath: string, operation: () => Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
39
|
+
//# sourceMappingURL=file-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../../src/settings/file-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAeH,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,4EAA4E;AAC5E,eAAO,MAAM,4BAA4B,4BAA4B,CAAC;AA4JtE;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,CAAC,CAAC,CA2BZ"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process file lock — the same protocol the ZCode desktop app uses.
|
|
3
|
+
*
|
|
4
|
+
* The settings API writes config files the desktop app also writes. An atomic
|
|
5
|
+
* rename makes each individual write safe, but it does NOT make two writers
|
|
6
|
+
* mutually exclusive: a settings write that lands while the app is writing the
|
|
7
|
+
* same file silently discards the app's change. Sharing the app's lock is the
|
|
8
|
+
* only way to avoid that, so this module re-implements its protocol exactly
|
|
9
|
+
* (ADR-0026):
|
|
10
|
+
*
|
|
11
|
+
* - lock directory `<path>.lock`, created with `mkdir` (atomic on POSIX)
|
|
12
|
+
* - one `owner-<pid>-<ts>-<rand>.json` per holder, written `wx`
|
|
13
|
+
* - a holder is authoritative only while its own owner file is the sole
|
|
14
|
+
* entry and the directory identity (dev+ino) has not changed
|
|
15
|
+
* - stale reclaim: owner pid no longer alive, or an ownerless lock past the
|
|
16
|
+
* grace window. Never steal from a live holder.
|
|
17
|
+
*
|
|
18
|
+
* Timestamps in owner files are validated against a clock-skew ceiling so a
|
|
19
|
+
* malformed or future-dated `createdAt` cannot keep a lock permanently
|
|
20
|
+
* unreclaimable.
|
|
21
|
+
*
|
|
22
|
+
* Everything is best-effort in the caller's favour: reaching the wait cap
|
|
23
|
+
* throws a lock-timeout error rather than deleting a newer writer's lock.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdir, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
28
|
+
/** Retry ladder while waiting for a contended lock (ms, last value repeats). */
|
|
29
|
+
const LOCK_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
|
|
30
|
+
/** How long an ownerless lock (no pid) must sit before it is considered stale. */
|
|
31
|
+
const DEFAULT_OWNERLESS_GRACE_MS = 100;
|
|
32
|
+
/** Total wait before giving up. Mirrors the upstream's 8s. */
|
|
33
|
+
const DEFAULT_MAX_WAIT_MS = 8_000;
|
|
34
|
+
/** Timestamps further in the future than this are treated as unusable. */
|
|
35
|
+
const MAX_CLOCK_SKEW_MS = 5 * 60_000;
|
|
36
|
+
/** Error code for a lock that could not be acquired within the wait cap. */
|
|
37
|
+
export const FILE_LOCK_TIMEOUT_ERROR_CODE = "ZCODE_FILE_LOCK_TIMEOUT";
|
|
38
|
+
function errorCode(error) {
|
|
39
|
+
if (typeof error !== "object" || error === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
const code = error.code;
|
|
42
|
+
return typeof code === "string" ? code : undefined;
|
|
43
|
+
}
|
|
44
|
+
function isFileExistsError(error) {
|
|
45
|
+
return errorCode(error) === "EEXIST";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A usable timestamp: finite, non-negative, not beyond the skew ceiling. A
|
|
49
|
+
* garbage `createdAt` makes the caller fall back to the file's mtime.
|
|
50
|
+
*/
|
|
51
|
+
function parseLockTimestamp(value, observedAt) {
|
|
52
|
+
if (typeof value !== "number")
|
|
53
|
+
return null;
|
|
54
|
+
if (!Number.isFinite(value) || value < 0)
|
|
55
|
+
return null;
|
|
56
|
+
if (value > observedAt + MAX_CLOCK_SKEW_MS)
|
|
57
|
+
return null;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function parseLockMetadata(raw, observedAt) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
return {
|
|
64
|
+
createdAt: parseLockTimestamp(parsed.createdAt, observedAt),
|
|
65
|
+
// Only a positive safe integer carries owner semantics: 0, negatives and
|
|
66
|
+
// fractions reach process.kill as junk and a dead lock would look alive.
|
|
67
|
+
pid: typeof parsed.pid === "number" && Number.isSafeInteger(parsed.pid) && parsed.pid > 0
|
|
68
|
+
? parsed.pid
|
|
69
|
+
: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { createdAt: null, pid: null };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** `process.kill(pid, 0)` liveness probe; EPERM means the process exists. */
|
|
77
|
+
function isProcessAlive(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return errorCode(error) !== "ESRCH";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function isOwnerFileReclaimable(ownerFile, ownerlessGraceMs, observedAt) {
|
|
87
|
+
const raw = await readFile(ownerFile, "utf-8");
|
|
88
|
+
const metadata = parseLockMetadata(raw, observedAt);
|
|
89
|
+
let createdAt = metadata.createdAt;
|
|
90
|
+
if (createdAt === null) {
|
|
91
|
+
const ownerStat = await stat(ownerFile);
|
|
92
|
+
createdAt = parseLockTimestamp(ownerStat.mtimeMs, observedAt) ?? observedAt;
|
|
93
|
+
}
|
|
94
|
+
const ownerExited = metadata.pid !== null && !isProcessAlive(metadata.pid);
|
|
95
|
+
const ownerlessLockIsStale = metadata.pid === null && observedAt - createdAt >= ownerlessGraceMs;
|
|
96
|
+
return ownerExited || ownerlessLockIsStale;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reclaim an abandoned lock. Conservative by construction: it only removes
|
|
100
|
+
* entries it has positively determined to be stale, and any read/stat failure
|
|
101
|
+
* reads as "not removed" so the caller keeps waiting instead of destroying a
|
|
102
|
+
* lock it does not understand.
|
|
103
|
+
*/
|
|
104
|
+
async function removeAbandonedLock(lockFile, ownerlessGraceMs) {
|
|
105
|
+
const observedAt = Date.now();
|
|
106
|
+
try {
|
|
107
|
+
const lockStat = await stat(lockFile);
|
|
108
|
+
if (lockStat.isDirectory()) {
|
|
109
|
+
const entries = await readdir(lockFile);
|
|
110
|
+
const owners = entries.filter((e) => e.startsWith("owner-") && e.endsWith(".json"));
|
|
111
|
+
if (owners.length === 1) {
|
|
112
|
+
// A single owner file is the ownership check: deleting it plus the
|
|
113
|
+
// directory can only ever reclaim that holder's lock.
|
|
114
|
+
if (!(await isOwnerFileReclaimable(join(lockFile, owners[0]), ownerlessGraceMs, observedAt))) {
|
|
115
|
+
return { removed: false };
|
|
116
|
+
}
|
|
117
|
+
await rm(join(lockFile, owners[0]), { force: true });
|
|
118
|
+
await rmdir(lockFile);
|
|
119
|
+
return { removed: true };
|
|
120
|
+
}
|
|
121
|
+
const directoryTimestamp = parseLockTimestamp(lockStat.mtimeMs, observedAt) ?? observedAt;
|
|
122
|
+
if (observedAt - directoryTimestamp < ownerlessGraceMs) {
|
|
123
|
+
return { removed: false };
|
|
124
|
+
}
|
|
125
|
+
for (const owner of owners) {
|
|
126
|
+
if (!(await isOwnerFileReclaimable(join(lockFile, owner), ownerlessGraceMs, observedAt))) {
|
|
127
|
+
return { removed: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// An empty directory (crash between mkdir and owner write) or a corrupt
|
|
131
|
+
// directory with several owners. Only the entries observed as stale are
|
|
132
|
+
// removed; a concurrent new owner makes the rmdir fail harmlessly.
|
|
133
|
+
await Promise.all(entries.map((e) => rm(join(lockFile, e), { force: true })));
|
|
134
|
+
await rmdir(lockFile);
|
|
135
|
+
return { removed: true };
|
|
136
|
+
}
|
|
137
|
+
// Legacy single-file lock from before the directory form. Re-read before
|
|
138
|
+
// deleting so a lock replaced meanwhile is not the one removed.
|
|
139
|
+
const raw = await readFile(lockFile, "utf-8");
|
|
140
|
+
if (!(await isOwnerFileReclaimable(lockFile, ownerlessGraceMs, observedAt))) {
|
|
141
|
+
return { removed: false };
|
|
142
|
+
}
|
|
143
|
+
if ((await readFile(lockFile, "utf-8")) !== raw)
|
|
144
|
+
return { removed: false };
|
|
145
|
+
await rm(lockFile, { force: true });
|
|
146
|
+
return { removed: true };
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// ENOENT: somebody else already reclaimed it — retry the acquire.
|
|
150
|
+
// ENOTEMPTY: a new owner appeared mid-sweep — keep waiting.
|
|
151
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTEMPTY") {
|
|
152
|
+
return { removed: false };
|
|
153
|
+
}
|
|
154
|
+
return { removed: false, error };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function createLockTimeoutError(filePath, waitedMs, cause) {
|
|
158
|
+
const error = new Error(`Timed out after ${waitedMs}ms waiting for the ZCode file lock: ${filePath}`);
|
|
159
|
+
error.code = FILE_LOCK_TIMEOUT_ERROR_CODE;
|
|
160
|
+
error.cause = cause;
|
|
161
|
+
return error;
|
|
162
|
+
}
|
|
163
|
+
// Process-internal FIFO in front of the OS lock: dozens of in-process callers
|
|
164
|
+
// polling a directory lock is a thundering herd, and the back of the queue
|
|
165
|
+
// would keep hitting the wait cap long after the actual write finished.
|
|
166
|
+
const processFileLockTails = new Map();
|
|
167
|
+
/**
|
|
168
|
+
* Run `operation` while holding ZCode's lock for `filePath`.
|
|
169
|
+
*
|
|
170
|
+
* @throws with code {@link FILE_LOCK_TIMEOUT_ERROR_CODE} when the lock could
|
|
171
|
+
* not be acquired within `lockMaxWaitMs`.
|
|
172
|
+
*/
|
|
173
|
+
export async function withFileLock(filePath, operation, options = {}) {
|
|
174
|
+
const previousTail = processFileLockTails.get(filePath) ?? Promise.resolve();
|
|
175
|
+
let releaseProcessQueue;
|
|
176
|
+
const currentTail = new Promise((resolve) => {
|
|
177
|
+
releaseProcessQueue = resolve;
|
|
178
|
+
});
|
|
179
|
+
processFileLockTails.set(filePath, currentTail);
|
|
180
|
+
await previousTail;
|
|
181
|
+
let releaseLock;
|
|
182
|
+
try {
|
|
183
|
+
// The lock directory sits NEXT TO the file, so the parent must exist first
|
|
184
|
+
// — `mkdir` of `<missing-dir>/x.lock` fails with ENOENT, which the acquire
|
|
185
|
+
// loop would misread as a lost race.
|
|
186
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
187
|
+
releaseLock = await acquireFileLock(filePath, options);
|
|
188
|
+
return await operation();
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
try {
|
|
192
|
+
await releaseLock?.();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
releaseProcessQueue();
|
|
196
|
+
if (processFileLockTails.get(filePath) === currentTail) {
|
|
197
|
+
processFileLockTails.delete(filePath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function acquireFileLock(filePath, options) {
|
|
203
|
+
const lockFile = `${filePath}.lock`;
|
|
204
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
205
|
+
const ownerFile = join(lockFile, `owner-${token}.json`);
|
|
206
|
+
const payload = `${JSON.stringify({
|
|
207
|
+
pid: process.pid,
|
|
208
|
+
createdAt: Date.now(),
|
|
209
|
+
token,
|
|
210
|
+
})}\n`;
|
|
211
|
+
const startedAt = Date.now();
|
|
212
|
+
const maxWaitMs = options.lockMaxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
213
|
+
const ownerlessGraceMs = Math.min(Math.max(options.lockOwnerlessGraceMs ?? DEFAULT_OWNERLESS_GRACE_MS, 0), Math.max(Math.floor(maxWaitMs / 2), 0));
|
|
214
|
+
const retryDelaysMs = options.lockRetryDelaysMs ?? LOCK_RETRY_DELAYS_MS;
|
|
215
|
+
let lastRemovalError;
|
|
216
|
+
for (let attempt = 0;; attempt += 1) {
|
|
217
|
+
let createdLock = false;
|
|
218
|
+
try {
|
|
219
|
+
await mkdir(lockFile);
|
|
220
|
+
createdLock = true;
|
|
221
|
+
const createdLockStat = await stat(lockFile);
|
|
222
|
+
await writeFile(ownerFile, payload, { encoding: "utf-8", flag: "wx" });
|
|
223
|
+
const currentLockStat = await stat(lockFile);
|
|
224
|
+
const currentOwners = (await readdir(lockFile)).filter((e) => e.startsWith("owner-") && e.endsWith(".json"));
|
|
225
|
+
// Ownership re-validation after the writes: the directory must still be
|
|
226
|
+
// the one we created and our owner file must be its only entry.
|
|
227
|
+
if (currentLockStat.dev !== createdLockStat.dev ||
|
|
228
|
+
currentLockStat.ino !== createdLockStat.ino ||
|
|
229
|
+
currentOwners.length !== 1 ||
|
|
230
|
+
currentOwners[0] !== `owner-${token}.json`) {
|
|
231
|
+
throw Object.assign(new Error("lock ownership changed during acquire"), {
|
|
232
|
+
code: "EEXIST",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return async () => {
|
|
236
|
+
// Only ever remove our own owner file: a lock that has since been
|
|
237
|
+
// taken over belongs to a different token and must survive.
|
|
238
|
+
await rm(ownerFile, { force: true }).catch(() => { });
|
|
239
|
+
await rmdir(lockFile).catch(() => {
|
|
240
|
+
// best-effort cleanup — a new owner makes this fail harmlessly
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (createdLock) {
|
|
246
|
+
await rm(ownerFile, { force: true }).catch(() => { });
|
|
247
|
+
await rmdir(lockFile).catch(() => { });
|
|
248
|
+
}
|
|
249
|
+
const lostCreatedLock = createdLock && errorCode(error) === "ENOENT";
|
|
250
|
+
if (!isFileExistsError(error) && !lostCreatedLock) {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
// How long WE have waited proves nothing about whether the CURRENT lock
|
|
254
|
+
// is stale, so the wait cap is a hard stop: reaching it times out rather
|
|
255
|
+
// than sweeping a lock a newer writer may have just installed.
|
|
256
|
+
const elapsedMs = Date.now() - startedAt;
|
|
257
|
+
if (elapsedMs >= maxWaitMs) {
|
|
258
|
+
const removalErrorCode = errorCode(lastRemovalError);
|
|
259
|
+
if (removalErrorCode === "EACCES" || removalErrorCode === "EPERM") {
|
|
260
|
+
throw lastRemovalError;
|
|
261
|
+
}
|
|
262
|
+
throw createLockTimeoutError(filePath, elapsedMs, error);
|
|
263
|
+
}
|
|
264
|
+
const removalAttempt = await removeAbandonedLock(lockFile, ownerlessGraceMs);
|
|
265
|
+
if (removalAttempt.removed)
|
|
266
|
+
continue;
|
|
267
|
+
if (removalAttempt.error !== undefined)
|
|
268
|
+
lastRemovalError = removalAttempt.error;
|
|
269
|
+
const remainingMs = Math.max(maxWaitMs - elapsedMs, 0);
|
|
270
|
+
if (retryDelaysMs.length === 0 || remainingMs === 0) {
|
|
271
|
+
const removalErrorCode = errorCode(lastRemovalError);
|
|
272
|
+
if (removalErrorCode === "EACCES" || removalErrorCode === "EPERM") {
|
|
273
|
+
throw lastRemovalError;
|
|
274
|
+
}
|
|
275
|
+
throw createLockTimeoutError(filePath, elapsedMs, lastRemovalError ?? error);
|
|
276
|
+
}
|
|
277
|
+
const retryDelayMs = retryDelaysMs[Math.min(attempt, retryDelaysMs.length - 1)] ?? remainingMs;
|
|
278
|
+
await sleep(Math.min(retryDelayMs, remainingMs));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../../src/settings/file-lock.ts"],"names":[],"mappings":"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"}
|