@hasna/economy 0.5.3 → 0.5.4
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 +6 -0
- package/README.md +4 -2
- package/dist/chunks/{index-s9mvven6.js → index-9arv0b1j.js} +161 -35
- package/dist/chunks/{index-mhrkwvtk.js → index-ct3zdefz.js} +1 -1
- package/dist/chunks/{index-1vz1arz5.js → index-n1wwqxfe.js} +1 -1
- package/dist/chunks/{index-bn04v378.js → index-nheq2nrz.js} +1 -1
- package/dist/chunks/{serve-8yqe7nhs.js → serve-ra46ms69.js} +2 -2
- package/dist/chunks/{tui-fe94bvp9.js → tui-a1tvcw2j.js} +3 -3
- package/dist/chunks/{watch-ct8dvrpr.js → watch-rvd3dn6r.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/lib/accounts-store.d.ts +25 -1
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/subscriptions-registry.d.ts +26 -0
- package/dist/lib/subscriptions-registry.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +3 -2
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -1
- package/dist/mcp/index.js +228 -102
- package/dist/otel/index.js +56 -27
- package/dist/server/index.js +169 -45
- package/dist/server/service-worker.js +160 -36
- package/package.json +1 -1
package/dist/otel/index.js
CHANGED
|
@@ -2384,7 +2384,7 @@ import { join as join3, basename } from "path";
|
|
|
2384
2384
|
// src/lib/accounts-store.ts
|
|
2385
2385
|
import { homedir as homedir2 } from "os";
|
|
2386
2386
|
import { join as join2 } from "path";
|
|
2387
|
-
import { z } from "zod";
|
|
2387
|
+
import { z as z2 } from "zod";
|
|
2388
2388
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
2389
2389
|
|
|
2390
2390
|
// src/lib/cloud-storage.ts
|
|
@@ -2515,32 +2515,61 @@ function economyCloudStorage(env = process.env) {
|
|
|
2515
2515
|
return cache;
|
|
2516
2516
|
}
|
|
2517
2517
|
|
|
2518
|
-
// src/lib/
|
|
2519
|
-
|
|
2518
|
+
// src/lib/subscriptions-registry.ts
|
|
2519
|
+
import { execFile } from "child_process";
|
|
2520
|
+
import { promisify } from "util";
|
|
2521
|
+
import { isAbsolute } from "path";
|
|
2522
|
+
import { z } from "zod";
|
|
2523
|
+
var execute = promisify(execFile);
|
|
2524
|
+
var nonempty = z.string().min(1);
|
|
2520
2525
|
var profileSchema = z.object({
|
|
2521
|
-
name:
|
|
2522
|
-
tool:
|
|
2526
|
+
name: nonempty,
|
|
2527
|
+
tool: nonempty,
|
|
2523
2528
|
email: z.string().optional(),
|
|
2524
2529
|
displayName: z.string().optional(),
|
|
2525
|
-
dir:
|
|
2530
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
2531
|
+
createdAt: nonempty,
|
|
2532
|
+
lastUsedAt: z.string().optional(),
|
|
2526
2533
|
description: z.string().optional(),
|
|
2527
|
-
|
|
2528
|
-
|
|
2534
|
+
active: z.boolean(),
|
|
2535
|
+
applied: z.boolean(),
|
|
2536
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
2529
2537
|
});
|
|
2530
|
-
var
|
|
2531
|
-
id:
|
|
2532
|
-
label:
|
|
2533
|
-
envVar:
|
|
2534
|
-
defaultDir:
|
|
2535
|
-
bin:
|
|
2538
|
+
var toolsSchema = z.array(z.object({
|
|
2539
|
+
id: nonempty,
|
|
2540
|
+
label: nonempty,
|
|
2541
|
+
envVar: nonempty,
|
|
2542
|
+
defaultDir: nonempty,
|
|
2543
|
+
bin: nonempty
|
|
2544
|
+
}));
|
|
2545
|
+
|
|
2546
|
+
// src/lib/accounts-store.ts
|
|
2547
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
2548
|
+
var profileSchema2 = z2.object({
|
|
2549
|
+
name: z2.string(),
|
|
2550
|
+
tool: z2.string(),
|
|
2551
|
+
email: z2.string().optional(),
|
|
2552
|
+
displayName: z2.string().optional(),
|
|
2553
|
+
dir: z2.string(),
|
|
2554
|
+
description: z2.string().optional(),
|
|
2555
|
+
createdAt: z2.string(),
|
|
2556
|
+
lastUsedAt: z2.string().optional(),
|
|
2557
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
2536
2558
|
});
|
|
2537
|
-
var
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2559
|
+
var toolDefSchema = z2.object({
|
|
2560
|
+
id: z2.string(),
|
|
2561
|
+
label: z2.string(),
|
|
2562
|
+
envVar: z2.string(),
|
|
2563
|
+
defaultDir: z2.string(),
|
|
2564
|
+
bin: z2.string()
|
|
2565
|
+
});
|
|
2566
|
+
var storeSchema = z2.object({
|
|
2567
|
+
version: z2.literal(1),
|
|
2568
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
2569
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
2570
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
2571
|
+
profiles: z2.array(profileSchema2).default([]),
|
|
2572
|
+
tools: z2.array(toolDefSchema).default([])
|
|
2544
2573
|
});
|
|
2545
2574
|
var BUILTIN_TOOLS = [
|
|
2546
2575
|
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
@@ -3345,11 +3374,11 @@ async function withIngestJournalLock(directory, action) {
|
|
|
3345
3374
|
}
|
|
3346
3375
|
|
|
3347
3376
|
// src/lib/ingest-journal-operation.ts
|
|
3348
|
-
import { z as
|
|
3349
|
-
var ingestJournalOperationSchema =
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3377
|
+
import { z as z3 } from "zod";
|
|
3378
|
+
var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
|
|
3379
|
+
z3.object({ action: z3.literal("status") }).strict(),
|
|
3380
|
+
z3.object({ action: z3.literal("reconcile") }).strict(),
|
|
3381
|
+
z3.object({ action: z3.literal("maintain"), confirm: z3.literal(true), limit: z3.number().int().min(1).max(32).default(32) }).strict()
|
|
3353
3382
|
]);
|
|
3354
3383
|
|
|
3355
3384
|
// src/lib/cloud-ingest.ts
|
|
@@ -3458,7 +3487,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3458
3487
|
// package.json
|
|
3459
3488
|
var package_default = {
|
|
3460
3489
|
name: "@hasna/economy",
|
|
3461
|
-
version: "0.5.
|
|
3490
|
+
version: "0.5.4",
|
|
3462
3491
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
3463
3492
|
repository: {
|
|
3464
3493
|
type: "git",
|
package/dist/server/index.js
CHANGED
|
@@ -4620,7 +4620,7 @@ __export(exports_legacy_plan, {
|
|
|
4620
4620
|
});
|
|
4621
4621
|
import { createHash as createHash6 } from "crypto";
|
|
4622
4622
|
import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
|
|
4623
|
-
import { isAbsolute } from "path";
|
|
4623
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
4624
4624
|
function object(value, fields, label) {
|
|
4625
4625
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== fields.sort().join(","))
|
|
4626
4626
|
throw Error(`${label} has unsupported or missing fields`);
|
|
@@ -4673,7 +4673,7 @@ function parseLegacySelection(value) {
|
|
|
4673
4673
|
return { schema: mode.selection, items };
|
|
4674
4674
|
}
|
|
4675
4675
|
function readLegacySelectionFile(path) {
|
|
4676
|
-
if (!
|
|
4676
|
+
if (!isAbsolute2(path) || path.includes("\x00"))
|
|
4677
4677
|
throw Error("--input must be an absolute path to a regular JSON file");
|
|
4678
4678
|
let fd;
|
|
4679
4679
|
try {
|
|
@@ -4708,7 +4708,7 @@ function readLegacySelectionFile(path) {
|
|
|
4708
4708
|
}
|
|
4709
4709
|
}
|
|
4710
4710
|
function parseLegacyPlanOptions(args) {
|
|
4711
|
-
if (args.length !== 2 || args[0] !== "--input" || !args[1] || !
|
|
4711
|
+
if (args.length !== 2 || args[0] !== "--input" || !args[1] || !isAbsolute2(args[1]) || args[1].includes("\x00"))
|
|
4712
4712
|
throw Error("legacy-plan accepts exactly --input <absolute JSON file>");
|
|
4713
4713
|
return args[1];
|
|
4714
4714
|
}
|
|
@@ -4837,7 +4837,7 @@ __export(exports_legacy_preservation, {
|
|
|
4837
4837
|
});
|
|
4838
4838
|
import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
|
|
4839
4839
|
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
|
|
4840
|
-
import { basename as basename4, dirname as dirname4, isAbsolute as
|
|
4840
|
+
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
|
|
4841
4841
|
function parseLegacyPlan(value) {
|
|
4842
4842
|
const p = record(value);
|
|
4843
4843
|
const mode = legacyMode("plan", p.schema);
|
|
@@ -4988,7 +4988,7 @@ async function preserveLegacyOriginals(client, input) {
|
|
|
4988
4988
|
});
|
|
4989
4989
|
}
|
|
4990
4990
|
function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
4991
|
-
if (!
|
|
4991
|
+
if (!isAbsolute3(path) || path.includes("\x00"))
|
|
4992
4992
|
fail("Artifact path must be absolute");
|
|
4993
4993
|
let fd;
|
|
4994
4994
|
try {
|
|
@@ -5027,7 +5027,7 @@ function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
|
5027
5027
|
}
|
|
5028
5028
|
function writeLegacyCapsule(path, input) {
|
|
5029
5029
|
const capsule = parseLegacyCapsule(input);
|
|
5030
|
-
if (!
|
|
5030
|
+
if (!isAbsolute3(path) || path.includes("\x00") || realpathSync(dirname4(path)) !== dirname4(path))
|
|
5031
5031
|
fail("Output requires an absolute path with a real parent directory");
|
|
5032
5032
|
const parent = lstatSync(dirname4(path));
|
|
5033
5033
|
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 63) !== 0)
|
|
@@ -5141,7 +5141,7 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
|
|
|
5141
5141
|
}
|
|
5142
5142
|
function parseLegacyPreservationOptions(command, args) {
|
|
5143
5143
|
const preserve = command === "legacy-preserve";
|
|
5144
|
-
if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !
|
|
5144
|
+
if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute3(args[1]) || preserve && (args[2] !== "--output" || !args[3] || !isAbsolute3(args[3])) || args.some((x) => x.includes("\x00")))
|
|
5145
5145
|
fail("Expected " + command + " --input <absolute JSON file>" + (preserve ? " --output <new file in an owner-only directory>" : ""));
|
|
5146
5146
|
return { input: args[1], ...preserve ? { output: args[3] } : {} };
|
|
5147
5147
|
}
|
|
@@ -5190,7 +5190,7 @@ __export(exports_legacy_operations, {
|
|
|
5190
5190
|
LegacyOperationInputError: () => LegacyOperationInputError
|
|
5191
5191
|
});
|
|
5192
5192
|
import { createHash as createHash8 } from "crypto";
|
|
5193
|
-
import { isAbsolute as
|
|
5193
|
+
import { isAbsolute as isAbsolute4 } from "path";
|
|
5194
5194
|
function parseLegacyIntent(action, value) {
|
|
5195
5195
|
try {
|
|
5196
5196
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5435,7 +5435,7 @@ function parseLegacyOperationOptions(command, args) {
|
|
|
5435
5435
|
const apply = command === "legacy-apply";
|
|
5436
5436
|
if (!apply && command !== "legacy-rollback")
|
|
5437
5437
|
fail2("Unknown legacy operation or arguments");
|
|
5438
|
-
if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !
|
|
5438
|
+
if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute4(args[1]) || apply && (args[2] !== "--capsule" || !args[3] || !isAbsolute4(args[3])) || args.some((s) => s.includes("\x00")))
|
|
5439
5439
|
fail2("Expected " + command + " --input <absolute intent JSON>" + (apply ? " --capsule <absolute preservation JSON>" : ""));
|
|
5440
5440
|
return { input: args[1], ...apply ? { capsule: args[3] } : {} };
|
|
5441
5441
|
}
|
|
@@ -5529,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5529
5529
|
// package.json
|
|
5530
5530
|
var package_default = {
|
|
5531
5531
|
name: "@hasna/economy",
|
|
5532
|
-
version: "0.5.
|
|
5532
|
+
version: "0.5.4",
|
|
5533
5533
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5534
5534
|
repository: {
|
|
5535
5535
|
type: "git",
|
|
@@ -8819,7 +8819,7 @@ import { join as join5, basename } from "path";
|
|
|
8819
8819
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
8820
8820
|
import { homedir as homedir2 } from "os";
|
|
8821
8821
|
import { join as join4 } from "path";
|
|
8822
|
-
import { z } from "zod";
|
|
8822
|
+
import { z as z2 } from "zod";
|
|
8823
8823
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
8824
8824
|
|
|
8825
8825
|
// src/lib/cloud-storage.ts
|
|
@@ -8838,33 +8838,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
|
8838
8838
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
8839
8839
|
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
8840
8840
|
|
|
8841
|
-
// src/lib/
|
|
8842
|
-
|
|
8843
|
-
|
|
8841
|
+
// src/lib/subscriptions-registry.ts
|
|
8842
|
+
import { execFile } from "child_process";
|
|
8843
|
+
import { promisify } from "util";
|
|
8844
|
+
import { isAbsolute } from "path";
|
|
8845
|
+
import { z } from "zod";
|
|
8846
|
+
var execute = promisify(execFile);
|
|
8847
|
+
var readSubscriptionsMetadata = async (args, env) => {
|
|
8848
|
+
let stdout;
|
|
8849
|
+
try {
|
|
8850
|
+
const result = await execute("subscriptions", args, {
|
|
8851
|
+
env,
|
|
8852
|
+
encoding: "utf8",
|
|
8853
|
+
timeout: 30000,
|
|
8854
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
8855
|
+
windowsHide: true
|
|
8856
|
+
});
|
|
8857
|
+
stdout = result.stdout;
|
|
8858
|
+
} catch (error) {
|
|
8859
|
+
const code = error?.code;
|
|
8860
|
+
if (code === "ENOENT")
|
|
8861
|
+
throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
|
|
8862
|
+
const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
|
|
8863
|
+
throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
|
|
8864
|
+
}
|
|
8865
|
+
try {
|
|
8866
|
+
return JSON.parse(stdout);
|
|
8867
|
+
} catch {
|
|
8868
|
+
throw new Error("subscriptions: metadata command returned invalid JSON");
|
|
8869
|
+
}
|
|
8870
|
+
};
|
|
8871
|
+
var nonempty = z.string().min(1);
|
|
8844
8872
|
var profileSchema = z.object({
|
|
8845
|
-
name:
|
|
8846
|
-
tool:
|
|
8873
|
+
name: nonempty,
|
|
8874
|
+
tool: nonempty,
|
|
8847
8875
|
email: z.string().optional(),
|
|
8848
8876
|
displayName: z.string().optional(),
|
|
8849
|
-
dir:
|
|
8877
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
8878
|
+
createdAt: nonempty,
|
|
8879
|
+
lastUsedAt: z.string().optional(),
|
|
8850
8880
|
description: z.string().optional(),
|
|
8851
|
-
|
|
8852
|
-
|
|
8881
|
+
active: z.boolean(),
|
|
8882
|
+
applied: z.boolean(),
|
|
8883
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
8853
8884
|
});
|
|
8854
|
-
var
|
|
8855
|
-
id:
|
|
8856
|
-
label:
|
|
8857
|
-
envVar:
|
|
8858
|
-
defaultDir:
|
|
8859
|
-
bin:
|
|
8885
|
+
var toolsSchema = z.array(z.object({
|
|
8886
|
+
id: nonempty,
|
|
8887
|
+
label: nonempty,
|
|
8888
|
+
envVar: nonempty,
|
|
8889
|
+
defaultDir: nonempty,
|
|
8890
|
+
bin: nonempty
|
|
8891
|
+
}));
|
|
8892
|
+
function unique(rows, identity) {
|
|
8893
|
+
if (new Set(rows.map(identity)).size !== rows.length) {
|
|
8894
|
+
throw new Error("subscriptions: duplicate identity in metadata response");
|
|
8895
|
+
}
|
|
8896
|
+
return rows;
|
|
8897
|
+
}
|
|
8898
|
+
|
|
8899
|
+
class SubscriptionsRegistry {
|
|
8900
|
+
read;
|
|
8901
|
+
transport = "api";
|
|
8902
|
+
registry = "subscriptions";
|
|
8903
|
+
env;
|
|
8904
|
+
profiles;
|
|
8905
|
+
tools;
|
|
8906
|
+
constructor(baseUrl, env, read = readSubscriptionsMetadata) {
|
|
8907
|
+
this.read = read;
|
|
8908
|
+
this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
|
|
8909
|
+
}
|
|
8910
|
+
allProfiles() {
|
|
8911
|
+
return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
|
|
8912
|
+
const parsed = z.array(profileSchema).safeParse(raw);
|
|
8913
|
+
if (!parsed.success)
|
|
8914
|
+
throw new Error("subscriptions: invalid profile metadata response");
|
|
8915
|
+
return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
|
|
8916
|
+
});
|
|
8917
|
+
}
|
|
8918
|
+
async listProfiles(tool) {
|
|
8919
|
+
const rows = await this.allProfiles();
|
|
8920
|
+
return rows.filter((row) => !tool || row.tool === tool);
|
|
8921
|
+
}
|
|
8922
|
+
async findProfile(name, tool) {
|
|
8923
|
+
const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
|
|
8924
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
8925
|
+
}
|
|
8926
|
+
async appliedProfile(tool) {
|
|
8927
|
+
const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
|
|
8928
|
+
if (matches.length > 1)
|
|
8929
|
+
throw new Error("subscriptions: ambiguous station applied-profile binding");
|
|
8930
|
+
return matches[0];
|
|
8931
|
+
}
|
|
8932
|
+
async currentProfile(_tool) {
|
|
8933
|
+
return;
|
|
8934
|
+
}
|
|
8935
|
+
async listTools() {
|
|
8936
|
+
return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
|
|
8937
|
+
const parsed = toolsSchema.safeParse(raw);
|
|
8938
|
+
if (!parsed.success)
|
|
8939
|
+
throw new Error("subscriptions: invalid tool metadata response");
|
|
8940
|
+
return unique(parsed.data, (row) => row.id);
|
|
8941
|
+
});
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
|
|
8945
|
+
// src/lib/accounts-store.ts
|
|
8946
|
+
var ACCOUNTS_APP = "accounts";
|
|
8947
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
8948
|
+
var profileSchema2 = z2.object({
|
|
8949
|
+
name: z2.string(),
|
|
8950
|
+
tool: z2.string(),
|
|
8951
|
+
email: z2.string().optional(),
|
|
8952
|
+
displayName: z2.string().optional(),
|
|
8953
|
+
dir: z2.string(),
|
|
8954
|
+
description: z2.string().optional(),
|
|
8955
|
+
createdAt: z2.string(),
|
|
8956
|
+
lastUsedAt: z2.string().optional(),
|
|
8957
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
8860
8958
|
});
|
|
8861
|
-
var
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8959
|
+
var toolDefSchema = z2.object({
|
|
8960
|
+
id: z2.string(),
|
|
8961
|
+
label: z2.string(),
|
|
8962
|
+
envVar: z2.string(),
|
|
8963
|
+
defaultDir: z2.string(),
|
|
8964
|
+
bin: z2.string()
|
|
8965
|
+
});
|
|
8966
|
+
var storeSchema = z2.object({
|
|
8967
|
+
version: z2.literal(1),
|
|
8968
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
8969
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
8970
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
8971
|
+
profiles: z2.array(profileSchema2).default([]),
|
|
8972
|
+
tools: z2.array(toolDefSchema).default([])
|
|
8868
8973
|
});
|
|
8869
8974
|
function localRegistryAllowed(env = process.env) {
|
|
8870
8975
|
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
|
|
@@ -9146,25 +9251,34 @@ class ApiStore {
|
|
|
9146
9251
|
}
|
|
9147
9252
|
}
|
|
9148
9253
|
function resolveStore(env = process.env, options = {}) {
|
|
9254
|
+
const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
|
|
9255
|
+
if (legacy)
|
|
9256
|
+
return new ApiStore(legacy.baseUrl, legacy.apiKey);
|
|
9257
|
+
const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
|
|
9258
|
+
if (current)
|
|
9259
|
+
return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
|
|
9260
|
+
return unconfiguredStore(env);
|
|
9261
|
+
}
|
|
9262
|
+
function resolveRegistryTransport(app, env, credentials) {
|
|
9149
9263
|
const clientEnv = env;
|
|
9150
|
-
const credential = resolveCredential(
|
|
9264
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
9151
9265
|
if (credential === null) {
|
|
9152
9266
|
try {
|
|
9153
|
-
resolveClientTransport(
|
|
9267
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
9154
9268
|
} catch (err) {
|
|
9155
9269
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
9156
|
-
return
|
|
9270
|
+
return null;
|
|
9157
9271
|
throw err;
|
|
9158
9272
|
}
|
|
9159
|
-
return
|
|
9273
|
+
return null;
|
|
9160
9274
|
}
|
|
9161
9275
|
if (credential.tier === "pointer") {
|
|
9162
|
-
throw new Error(
|
|
9276
|
+
throw new Error(`${app}: ${credential.source} names a secrets-vault item, and the registry adapter resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + `hasna.credentials.${app}.api-key, ~/.hasna/${app}/config/credentials, or HASNA_${app.toUpperCase()}_API_KEY.`);
|
|
9163
9277
|
}
|
|
9164
|
-
const resolution = resolveClientTransport(
|
|
9165
|
-
credentials: { ...
|
|
9278
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
9279
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
9166
9280
|
});
|
|
9167
|
-
return
|
|
9281
|
+
return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
|
|
9168
9282
|
}
|
|
9169
9283
|
function isNoAccountsCredentialConfigurationError(error) {
|
|
9170
9284
|
if (typeof error !== "object" || error === null)
|
|
@@ -9181,10 +9295,10 @@ function unconfiguredStore(env) {
|
|
|
9181
9295
|
return new NoRegistryStore;
|
|
9182
9296
|
}
|
|
9183
9297
|
function accountsLocalModeNotice() {
|
|
9184
|
-
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014
|
|
9298
|
+
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 neither Accounts nor Subscriptions credentials resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "For hosted attribution, configure the Subscriptions CLI through its normal sign-in or credential resolver.";
|
|
9185
9299
|
}
|
|
9186
9300
|
function accountsRefusalNotice() {
|
|
9187
|
-
return "accounts:
|
|
9301
|
+
return "accounts: neither Accounts nor Subscriptions credentials resolved (legacy hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY; current hasna.credentials.subscriptions.api-key, " + "~/.hasna/subscriptions/config/credentials, HASNA_SUBSCRIPTIONS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
|
|
9188
9302
|
}
|
|
9189
9303
|
var accountsLocalNoticePrinted = false;
|
|
9190
9304
|
function announceAccountsLocalMode() {
|
|
@@ -9272,7 +9386,10 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
9272
9386
|
if (!configuredDir)
|
|
9273
9387
|
return null;
|
|
9274
9388
|
const normalized = normalizeDir(configuredDir);
|
|
9275
|
-
|
|
9389
|
+
const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
|
|
9390
|
+
if (matches.length > 1)
|
|
9391
|
+
throw new Error("account attribution: multiple profiles match the configured directory");
|
|
9392
|
+
return matches[0] ?? null;
|
|
9276
9393
|
}
|
|
9277
9394
|
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
9278
9395
|
const override = envOverride(agent, env);
|
|
@@ -9286,8 +9403,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
|
9286
9403
|
continue;
|
|
9287
9404
|
const envProfile = await profileForEnvDir(store, tool, env);
|
|
9288
9405
|
if (envProfile)
|
|
9289
|
-
return fromProfile(envProfile, "env");
|
|
9290
|
-
|
|
9406
|
+
return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
|
|
9407
|
+
if (store.registry === "subscriptions" && env[tool.envVar])
|
|
9408
|
+
return null;
|
|
9409
|
+
if (store.appliedProfile) {
|
|
9410
|
+
const applied = await store.appliedProfile(toolId);
|
|
9411
|
+
if (applied)
|
|
9412
|
+
return applied.switchedAway ? null : fromProfile(applied, "applied");
|
|
9413
|
+
}
|
|
9414
|
+
const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
|
|
9291
9415
|
if (appliedName) {
|
|
9292
9416
|
const applied = await store.findProfile(appliedName, toolId);
|
|
9293
9417
|
if (applied)
|