usagemax 0.1.0 → 0.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/README.md +1 -0
- package/package.json +3 -2
- package/src/cli.js +21 -7
- package/src/installation.js +38 -0
package/README.md
CHANGED
|
@@ -38,5 +38,6 @@ UsageMax uses [ccusage](https://github.com/ccusage/ccusage) for local source det
|
|
|
38
38
|
- Sync is one-shot. There is no resident scanner or high-frequency polling loop.
|
|
39
39
|
- After the first import, normal syncs inspect only the current and previous local day; use `sync --full` to reconcile older history.
|
|
40
40
|
- The collector key is written with user-only permissions where the operating system supports them.
|
|
41
|
+
- A separate private random installation ID survives collector rotation, relinking, and display-name changes. It is not a hardware fingerprint; the server stores only its SHA-256 hash. Concurrent or repeated syncs remain idempotent.
|
|
41
42
|
|
|
42
43
|
Use `USAGEMAX_CONFIG_DIR` to select another config directory. Development/self-hosted installations may set `USAGEMAX_LINK_ENDPOINT` before linking.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usagemax",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Link local coding-agent usage to your UsageMax profile",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "UsageMax",
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": {
|
|
18
|
-
"usagemax": "
|
|
18
|
+
"usagemax": "src/cli.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"src/cli.js",
|
|
22
22
|
"src/core.js",
|
|
23
|
+
"src/installation.js",
|
|
23
24
|
"README.md",
|
|
24
25
|
"LICENSE"
|
|
25
26
|
],
|
package/src/cli.js
CHANGED
|
@@ -10,10 +10,11 @@ import process from "node:process";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
|
|
12
12
|
import { batchId, buildDeltaPlan, normalizeLinkCode, sourceSummary, validHttpsUrl } from "./core.js";
|
|
13
|
+
import { stableInstallationId } from "./installation.js";
|
|
13
14
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const executeFile = promisify(execFile);
|
|
16
|
-
const VERSION = "0.1.
|
|
17
|
+
const VERSION = "0.1.1";
|
|
17
18
|
const DEFAULT_LINK_ENDPOINT = "https://terrific-bobcat-522.convex.site/v1/devices/link";
|
|
18
19
|
const CONFIG_FILE = "config.json";
|
|
19
20
|
const MAX_REPORT_BYTES = 100 * 1024 * 1024;
|
|
@@ -184,26 +185,32 @@ async function link(args) {
|
|
|
184
185
|
const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
|
|
185
186
|
if (!endpoint) throw new Error("USAGEMAX_LINK_ENDPOINT must use HTTPS, except for localhost development.");
|
|
186
187
|
const name = (option(args, "--name") || deviceLabel()).trim().slice(0, 80);
|
|
188
|
+
const previous = await readConfig();
|
|
189
|
+
const deviceId = await stableInstallationId(configDirectory(), previous?.deviceId);
|
|
190
|
+
const headers = { "content-type": "application/json" };
|
|
191
|
+
if (previous?.token) headers.authorization = `Bearer ${previous.token}`;
|
|
187
192
|
const response = await fetch(endpoint, {
|
|
188
193
|
method: "POST",
|
|
189
|
-
headers
|
|
190
|
-
body: JSON.stringify({ code, name, platform: platform(), cliVersion: VERSION }),
|
|
194
|
+
headers,
|
|
195
|
+
body: JSON.stringify({ code, name, platform: platform(), cliVersion: VERSION, deviceId }),
|
|
191
196
|
signal: AbortSignal.timeout(15_000),
|
|
192
197
|
});
|
|
193
198
|
const body = await response.json().catch(() => ({}));
|
|
194
199
|
if (!response.ok) throw new Error(body?.error === "invalid_or_expired_link_code" ? "That link code is invalid, expired, or already used." : "UsageMax could not link this computer.");
|
|
195
200
|
const ingestUrl = validHttpsUrl(body.ingestUrl, { allowLocalhost: true });
|
|
196
201
|
if (!/^umx_[a-f0-9]{64}$/.test(body.token || "") || !ingestUrl) throw new Error("UsageMax returned an invalid link response.");
|
|
202
|
+
const profileHandle = typeof body.profileHandle === "string" ? body.profileHandle : undefined;
|
|
203
|
+
const sameAccount = Boolean(previous && previous.profileHandle && previous.profileHandle === profileHandle);
|
|
197
204
|
const config = {
|
|
198
205
|
version: 1,
|
|
199
206
|
token: body.token,
|
|
200
207
|
ingestUrl,
|
|
201
208
|
profileUrl: validHttpsUrl(body.profileUrl) || "https://usagemax.com/account",
|
|
202
|
-
profileHandle
|
|
203
|
-
deviceId
|
|
209
|
+
profileHandle,
|
|
210
|
+
deviceId,
|
|
204
211
|
deviceName: name,
|
|
205
212
|
linkedAt: new Date().toISOString(),
|
|
206
|
-
snapshots: {},
|
|
213
|
+
snapshots: sameAccount ? previous.snapshots : {},
|
|
207
214
|
};
|
|
208
215
|
await writeConfig(config);
|
|
209
216
|
process.stdout.write(`Linked ${name} to ${config.profileHandle ? `@${config.profileHandle}` : "UsageMax"}.\n`);
|
|
@@ -218,6 +225,7 @@ async function link(args) {
|
|
|
218
225
|
async function sync(args, suppliedConfig) {
|
|
219
226
|
const config = suppliedConfig || await readConfig();
|
|
220
227
|
if (!config) throw new Error("This computer is not linked. Open https://usagemax.com/account and create a link code.");
|
|
228
|
+
config.deviceId = await stableInstallationId(configDirectory(), config.deviceId);
|
|
221
229
|
const full = args.includes("--full");
|
|
222
230
|
const inventory = await sourceInventory();
|
|
223
231
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -239,6 +247,7 @@ async function sync(args, suppliedConfig) {
|
|
|
239
247
|
authorization: `Bearer ${config.token}`,
|
|
240
248
|
"content-type": "application/json",
|
|
241
249
|
"idempotency-key": batchId(config.deviceId, events),
|
|
250
|
+
"x-usagemax-device-id": config.deviceId,
|
|
242
251
|
},
|
|
243
252
|
body: JSON.stringify({ events }),
|
|
244
253
|
signal: AbortSignal.timeout(30_000),
|
|
@@ -272,6 +281,11 @@ async function status() {
|
|
|
272
281
|
process.stdout.write("Not linked. Open https://usagemax.com/account to connect this computer.\n");
|
|
273
282
|
return;
|
|
274
283
|
}
|
|
284
|
+
const stableId = await stableInstallationId(configDirectory(), config.deviceId);
|
|
285
|
+
if (config.deviceId !== stableId) {
|
|
286
|
+
config.deviceId = stableId;
|
|
287
|
+
await writeConfig(config);
|
|
288
|
+
}
|
|
275
289
|
process.stdout.write(`Linked: ${config.deviceName || deviceLabel()}${config.profileHandle ? ` → @${config.profileHandle}` : ""}\n`);
|
|
276
290
|
process.stdout.write(`Last sync: ${config.lastSyncAt || "never"}\n`);
|
|
277
291
|
process.stdout.write(`Profile: ${config.profileUrl || "https://usagemax.com/account"}\n`);
|
|
@@ -307,7 +321,7 @@ async function removeLink() {
|
|
|
307
321
|
return;
|
|
308
322
|
}
|
|
309
323
|
await unlink(path);
|
|
310
|
-
process.stdout.write("Removed the local UsageMax collector key. Revoke the collector in your account if this computer is no longer trusted.\n");
|
|
324
|
+
process.stdout.write("Removed the local UsageMax collector key. This computer's private installation identity was retained so relinking cannot duplicate its usage. Revoke the collector in your account if this computer is no longer trusted.\n");
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
async function main() {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const INSTALLATION_FILE = "installation.json";
|
|
7
|
+
|
|
8
|
+
export function validInstallationId(value) {
|
|
9
|
+
return typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function stableInstallationId(directory, preferred) {
|
|
13
|
+
const path = join(directory, INSTALLATION_FILE);
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
16
|
+
if (validInstallationId(parsed?.id)) return parsed.id;
|
|
17
|
+
throw new Error(`UsageMax installation identity is invalid: ${path}`);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error?.code !== "ENOENT") throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const id = validInstallationId(preferred) ? preferred : randomUUID();
|
|
23
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
24
|
+
try {
|
|
25
|
+
await writeFile(path, `${JSON.stringify({ version: 1, id }, null, 2)}\n`, {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
mode: 0o600,
|
|
28
|
+
flag: "wx",
|
|
29
|
+
});
|
|
30
|
+
if (platform() !== "win32") await chmod(path, 0o600);
|
|
31
|
+
return id;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code !== "EEXIST") throw error;
|
|
34
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
35
|
+
if (validInstallationId(parsed?.id)) return parsed.id;
|
|
36
|
+
throw new Error(`UsageMax installation identity is invalid: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
}
|