@hasna/economy 0.4.0 → 0.5.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/CHANGELOG.md +31 -0
- package/README.md +12 -9
- package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
- package/dist/chunks/billing-bkpxy4kv.js +13 -0
- package/dist/chunks/config-j9mv3ehk.js +15 -0
- package/dist/chunks/index-2hwzmfgv.js +3407 -0
- package/dist/chunks/index-49f12css.js +124 -0
- package/dist/chunks/index-d480p2wh.js +1322 -0
- package/dist/chunks/index-ez9thsr7.js +3440 -0
- package/dist/chunks/index-hesxq90g.js +62 -0
- package/dist/chunks/index-k540hfww.js +258 -0
- package/dist/chunks/index-p35cyhc4.js +4 -0
- package/dist/chunks/index-r1jv3kpy.js +451 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-xsah93s2.js +2085 -0
- package/dist/chunks/index-zpvea08j.js +3372 -0
- package/dist/chunks/menubar-6xtkt17h.js +296 -0
- package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
- package/dist/chunks/pricing-f80p81gr.js +29 -0
- package/dist/chunks/pricing-vf2pycmh.js +30 -0
- package/dist/chunks/pricing-y21mcjsa.js +30 -0
- package/dist/chunks/serve-2knem98x.js +4655 -0
- package/dist/chunks/sqlite-store-501f32ts.js +258 -0
- package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
- package/dist/chunks/sqlite-store-st99garx.js +258 -0
- package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
- package/dist/chunks/tui-khmnkat2.js +22 -0
- package/dist/chunks/watch-4y064yys.js +196 -0
- package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/journal.d.ts +3 -0
- package/dist/cli/commands/journal.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +97 -4
- package/dist/cli/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +1358 -10114
- package/dist/db/cloud.d.ts +7 -9
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +14 -12
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/ingest-concurrency-worker.d.ts +2 -0
- package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
- package/dist/db/ingest-errors.d.ts +9 -0
- package/dist/db/ingest-errors.d.ts.map +1 -0
- package/dist/db/ingest-schema.d.ts +3 -0
- package/dist/db/ingest-schema.d.ts.map +1 -0
- package/dist/db/ingest-specs.d.ts +8 -0
- package/dist/db/ingest-specs.d.ts.map +1 -0
- package/dist/db/ingest-test-contract.d.ts +3 -0
- package/dist/db/ingest-test-contract.d.ts.map +1 -0
- package/dist/db/ingest-validation.d.ts +12 -0
- package/dist/db/ingest-validation.d.ts.map +1 -0
- package/dist/db/ingest.d.ts +27 -0
- package/dist/db/ingest.d.ts.map +1 -0
- package/dist/db/legacy-schema.d.ts +7 -0
- package/dist/db/legacy-schema.d.ts.map +1 -0
- package/dist/db/observation-contract.d.ts +3 -0
- package/dist/db/observation-contract.d.ts.map +1 -0
- package/dist/db/observation-provenance.d.ts +23 -0
- package/dist/db/observation-provenance.d.ts.map +1 -0
- package/dist/db/observation-schema.d.ts +24 -0
- package/dist/db/observation-schema.d.ts.map +1 -0
- package/dist/db/pg-connection.d.ts +10 -0
- package/dist/db/pg-connection.d.ts.map +1 -0
- package/dist/db/pg-legacy-schema.d.ts +12 -0
- package/dist/db/pg-legacy-schema.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +4 -9
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-protocol.d.ts +28 -0
- package/dist/db/pg-protocol.d.ts.map +1 -0
- package/dist/db/sqlite-store.d.ts +4 -0
- package/dist/db/sqlite-store.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +17 -21
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/db/third-party-sqlite.d.ts +10 -0
- package/dist/db/third-party-sqlite.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1171 -2465
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +25 -0
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/collection-outcome.d.ts +17 -0
- package/dist/ingest/collection-outcome.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/quota-observation.d.ts +16 -0
- package/dist/ingest/quota-observation.d.ts.map +1 -0
- package/dist/ingest/source-time.d.ts +4 -0
- package/dist/ingest/source-time.d.ts.map +1 -0
- package/dist/lib/accounts-store.d.ts +39 -13
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +1 -1
- package/dist/lib/autosync-gate.d.ts +12 -5
- package/dist/lib/autosync-gate.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +18 -40
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +2 -0
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/budget-decimal.d.ts +14 -0
- package/dist/lib/budget-decimal.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/ingest-batches.d.ts +21 -0
- package/dist/lib/ingest-batches.d.ts.map +1 -0
- package/dist/lib/ingest-journal-client.d.ts +37 -0
- package/dist/lib/ingest-journal-client.d.ts.map +1 -0
- package/dist/lib/ingest-journal-lock.d.ts +5 -0
- package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
- package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
- package/dist/lib/ingest-journal-operation.d.ts +55 -0
- package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
- package/dist/lib/ingest-journal.d.ts +73 -0
- package/dist/lib/ingest-journal.d.ts.map +1 -0
- package/dist/lib/money-display.d.ts +13 -0
- package/dist/lib/money-display.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +12 -0
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +14 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +64 -7
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +25 -21
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/summary-contract.d.ts +32 -0
- package/dist/lib/summary-contract.d.ts.map +1 -0
- package/dist/lib/summary-test-contract.d.ts +3 -0
- package/dist/lib/summary-test-contract.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +4 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts.map +1 -1
- package/dist/lib/sync-report.d.ts +4 -0
- package/dist/lib/sync-report.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +9 -0
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
- package/dist/mcp/agent-registry-store.d.ts +30 -0
- package/dist/mcp/agent-registry-store.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +9 -16
- package/dist/mcp/agent-registry.d.ts.map +1 -1
- package/dist/mcp/index.js +2291 -3458
- package/dist/mcp/ingest-journal-tool.d.ts +2 -0
- package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +1960 -289
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10639 -6720
- package/dist/server/legacy-inventory.d.ts +52 -0
- package/dist/server/legacy-inventory.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +64 -0
- package/dist/server/legacy-operations.d.ts.map +1 -0
- package/dist/server/legacy-plan.d.ts +55 -0
- package/dist/server/legacy-plan.d.ts.map +1 -0
- package/dist/server/legacy-preservation.d.ts +58 -0
- package/dist/server/legacy-preservation.d.ts.map +1 -0
- package/dist/server/pg-sync-worker.js +195 -21
- package/dist/server/serve.d.ts +20 -16
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/service-admission.d.ts +10 -0
- package/dist/server/service-admission.d.ts.map +1 -0
- package/dist/server/service-dispatcher.d.ts +45 -0
- package/dist/server/service-dispatcher.d.ts.map +1 -0
- package/dist/server/service-operations.d.ts +5 -0
- package/dist/server/service-operations.d.ts.map +1 -0
- package/dist/server/service-protocol.d.ts +50 -0
- package/dist/server/service-protocol.d.ts.map +1 -0
- package/dist/server/service-worker.d.ts +2 -0
- package/dist/server/service-worker.d.ts.map +1 -0
- package/dist/server/service-worker.js +8038 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/configuration.md +5 -5
- package/docs/cost-reporting.md +49 -0
- package/docs/historical-data-reconciliation.md +203 -0
- package/docs/ingest-journal.md +108 -0
- package/docs/ingestion.md +60 -4
- package/docs/mcp.md +2 -2
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/postgresql-legacy-schema-repair.md +30 -0
- package/docs/rest-api.md +2 -2
- package/package.json +25 -13
- package/dist/lib/test-hermetic-accounts.d.ts +0 -7
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
|
@@ -0,0 +1,4655 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
getServeBindHost,
|
|
4
|
+
isAuthorizedRequest
|
|
5
|
+
} from "./index-w3bvv0ns.js";
|
|
6
|
+
import {
|
|
7
|
+
syncAll
|
|
8
|
+
} from "./index-xsah93s2.js";
|
|
9
|
+
import {
|
|
10
|
+
buildBrief,
|
|
11
|
+
packageMetadata,
|
|
12
|
+
queryBillingDiff,
|
|
13
|
+
queryExportRows,
|
|
14
|
+
queryForecast,
|
|
15
|
+
queryModelEfficiency,
|
|
16
|
+
queryProjectDetail,
|
|
17
|
+
queryRangeStats,
|
|
18
|
+
querySavingsSummary
|
|
19
|
+
} from "./index-d480p2wh.js";
|
|
20
|
+
import {
|
|
21
|
+
AGENTS,
|
|
22
|
+
INGEST_SCHEMA_SQL,
|
|
23
|
+
IngestError,
|
|
24
|
+
LEGACY_PG_SQL,
|
|
25
|
+
OBSERVATION_PG_MIGRATIONS,
|
|
26
|
+
bulkIngest,
|
|
27
|
+
deleteBudget,
|
|
28
|
+
deleteGoal,
|
|
29
|
+
deleteModelPricing,
|
|
30
|
+
deleteProject,
|
|
31
|
+
deleteSubscription,
|
|
32
|
+
ensurePricingSeeded,
|
|
33
|
+
getBudgetStatuses,
|
|
34
|
+
getGoalStatuses,
|
|
35
|
+
getMachineId,
|
|
36
|
+
insertFeedback,
|
|
37
|
+
isAgent,
|
|
38
|
+
listMachineRegistry,
|
|
39
|
+
listMachines,
|
|
40
|
+
listModelPricing,
|
|
41
|
+
listProjects,
|
|
42
|
+
listSubscriptions,
|
|
43
|
+
queryAccountBreakdown,
|
|
44
|
+
queryAccountBreakdownSince,
|
|
45
|
+
queryAgentBreakdown,
|
|
46
|
+
queryAgentBreakdownSince,
|
|
47
|
+
queryBillingSummary,
|
|
48
|
+
queryCostCenterBreakdown,
|
|
49
|
+
queryDailyBreakdown,
|
|
50
|
+
queryHourlyBreakdown,
|
|
51
|
+
queryModelBreakdown,
|
|
52
|
+
queryModelBreakdownSince,
|
|
53
|
+
queryProjectBreakdown,
|
|
54
|
+
queryProjectBreakdownSince,
|
|
55
|
+
queryRequestsSince,
|
|
56
|
+
queryScanReceipts,
|
|
57
|
+
querySessions,
|
|
58
|
+
querySummary,
|
|
59
|
+
queryTopSessions,
|
|
60
|
+
queryUsageSnapshots,
|
|
61
|
+
reconcileIngestReceipt,
|
|
62
|
+
subscriptionId,
|
|
63
|
+
upsertBudget,
|
|
64
|
+
upsertGoal,
|
|
65
|
+
upsertModelPricing,
|
|
66
|
+
upsertProject,
|
|
67
|
+
upsertSubscription,
|
|
68
|
+
usageSnapshotFilterForPeriod
|
|
69
|
+
} from "./index-ez9thsr7.js";
|
|
70
|
+
import {
|
|
71
|
+
__require
|
|
72
|
+
} from "./index-p35cyhc4.js";
|
|
73
|
+
|
|
74
|
+
// src/db/cloud.ts
|
|
75
|
+
import pg2 from "pg";
|
|
76
|
+
|
|
77
|
+
// src/db/sync-pg.ts
|
|
78
|
+
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
79
|
+
import { fileURLToPath } from "url";
|
|
80
|
+
import { dirname, join } from "path";
|
|
81
|
+
import { existsSync } from "fs";
|
|
82
|
+
|
|
83
|
+
// src/db/dialect.ts
|
|
84
|
+
function translateSql(sql, dialect) {
|
|
85
|
+
if (dialect === "sqlite")
|
|
86
|
+
return sql;
|
|
87
|
+
return sqliteToPostgres(sql);
|
|
88
|
+
}
|
|
89
|
+
function translateParams(params) {
|
|
90
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
91
|
+
return flat.map((p) => p === undefined ? null : p);
|
|
92
|
+
}
|
|
93
|
+
function sqliteToPostgres(sql) {
|
|
94
|
+
let out = sql;
|
|
95
|
+
let paramIdx = 0;
|
|
96
|
+
out = out.replace(/\?/g, () => `$${++paramIdx}`);
|
|
97
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
|
|
98
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
|
|
99
|
+
const n = parseInt(amount, 10);
|
|
100
|
+
const absN = Math.abs(n);
|
|
101
|
+
const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
|
|
102
|
+
const plural = absN === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
|
103
|
+
return n < 0 ? `NOW() - INTERVAL '${absN} ${plural}'` : `NOW() + INTERVAL '${absN} ${plural}'`;
|
|
104
|
+
});
|
|
105
|
+
out = out.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
|
|
106
|
+
out = out.replace(/GROUP_CONCAT\s*\(/gi, "STRING_AGG(");
|
|
107
|
+
out = out.replace(/json_extract\s*\(\s*(\w+)\s*,\s*'\$\.(\w+)'\s*\)/gi, (_match, col, key) => `${col}->>'${key}'`);
|
|
108
|
+
out = out.replace(/\bAUTOINCREMENT\b/gi, "GENERATED ALWAYS AS IDENTITY");
|
|
109
|
+
out = out.replace(/(?<![I])LIKE/gi, "ILIKE");
|
|
110
|
+
out = out.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
|
|
111
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO\s+"?(\w+)"?\s*\(([^)]+)\)\s*VALUES/gi, (_match, table, colList) => `INSERT INTO "${table}" (${colList}) VALUES`);
|
|
112
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
|
|
113
|
+
if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(out)) {
|
|
114
|
+
out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
|
|
115
|
+
out = out.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/db/pg-protocol.ts
|
|
121
|
+
function pgNow() {
|
|
122
|
+
return performance.timeOrigin + performance.now();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/db/sync-pg.ts
|
|
126
|
+
function translateSqliteDates(sql) {
|
|
127
|
+
let s = sql;
|
|
128
|
+
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
129
|
+
s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
|
|
130
|
+
s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
|
|
131
|
+
s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
|
|
132
|
+
s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
|
|
133
|
+
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
134
|
+
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
135
|
+
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
136
|
+
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
137
|
+
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
|
|
138
|
+
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
|
|
139
|
+
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
140
|
+
});
|
|
141
|
+
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
142
|
+
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
143
|
+
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
144
|
+
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
function resolveWorkerPath() {
|
|
148
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
149
|
+
const selfExt = fileURLToPath(import.meta.url).endsWith(".js") ? "js" : "ts";
|
|
150
|
+
const otherExt = selfExt === "js" ? "ts" : "js";
|
|
151
|
+
const candidates = [
|
|
152
|
+
join(here, `pg-sync-worker.${selfExt}`),
|
|
153
|
+
join(here, `pg-sync-worker.${otherExt}`),
|
|
154
|
+
join(here, "..", "db", "pg-sync-worker.js")
|
|
155
|
+
];
|
|
156
|
+
for (const candidate of candidates)
|
|
157
|
+
if (existsSync(candidate))
|
|
158
|
+
return candidate;
|
|
159
|
+
return candidates[0];
|
|
160
|
+
}
|
|
161
|
+
function boundedTimeout(value, fallback) {
|
|
162
|
+
const timeout = value ?? fallback;
|
|
163
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 120000) {
|
|
164
|
+
throw new Error("PostgreSQL timeout must be an integer between 1 and 120000 milliseconds");
|
|
165
|
+
}
|
|
166
|
+
return timeout;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class SyncPgAdapter {
|
|
170
|
+
worker;
|
|
171
|
+
port;
|
|
172
|
+
lifecycle = new Int32Array(new SharedArrayBuffer(4));
|
|
173
|
+
sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
174
|
+
queryTimeoutMs;
|
|
175
|
+
transactionTimeoutMs;
|
|
176
|
+
counter = 0;
|
|
177
|
+
transactionId = null;
|
|
178
|
+
transactionDeadline = Infinity;
|
|
179
|
+
depth = 0;
|
|
180
|
+
closed = false;
|
|
181
|
+
constructor(dsn, options = {}) {
|
|
182
|
+
this.queryTimeoutMs = boundedTimeout(options.queryTimeoutMs, 30000);
|
|
183
|
+
this.transactionTimeoutMs = boundedTimeout(options.transactionTimeoutMs, 30000);
|
|
184
|
+
this.worker = new Worker(resolveWorkerPath(), { workerData: {
|
|
185
|
+
dsn,
|
|
186
|
+
lifecycle: this.lifecycle.buffer,
|
|
187
|
+
queryTimeoutMs: this.queryTimeoutMs,
|
|
188
|
+
transactionTimeoutMs: this.transactionTimeoutMs
|
|
189
|
+
} });
|
|
190
|
+
this.worker.on("error", () => {
|
|
191
|
+
this.abandon();
|
|
192
|
+
});
|
|
193
|
+
this.worker.on("exit", () => {
|
|
194
|
+
this.closed = true;
|
|
195
|
+
});
|
|
196
|
+
const channel = new MessageChannel;
|
|
197
|
+
this.port = channel.port1;
|
|
198
|
+
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
199
|
+
}
|
|
200
|
+
abandon() {
|
|
201
|
+
this.closed = true;
|
|
202
|
+
Atomics.store(this.lifecycle, 0, 1);
|
|
203
|
+
this.worker.terminate();
|
|
204
|
+
this.port.close();
|
|
205
|
+
}
|
|
206
|
+
request(operation, sql, params = []) {
|
|
207
|
+
if (this.closed || Atomics.load(this.lifecycle, 0) !== 0) {
|
|
208
|
+
if (!this.closed)
|
|
209
|
+
this.abandon();
|
|
210
|
+
throw new Error("PostgreSQL adapter is closed; reconcile any uncertain write before creating a new adapter");
|
|
211
|
+
}
|
|
212
|
+
const deadline = Math.min(pgNow() + this.queryTimeoutMs, this.transactionDeadline);
|
|
213
|
+
const id = ++this.counter;
|
|
214
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
215
|
+
if (deadline <= pgNow()) {
|
|
216
|
+
this.abandon();
|
|
217
|
+
throw new Error("PostgreSQL transaction deadline exceeded; transaction abandoned");
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
this.worker.postMessage({
|
|
221
|
+
id,
|
|
222
|
+
operation,
|
|
223
|
+
sql,
|
|
224
|
+
params,
|
|
225
|
+
deadline,
|
|
226
|
+
transactionId: this.transactionId,
|
|
227
|
+
depth: this.depth,
|
|
228
|
+
signal: signal.buffer
|
|
229
|
+
});
|
|
230
|
+
for (;; ) {
|
|
231
|
+
if (pgNow() >= deadline)
|
|
232
|
+
break;
|
|
233
|
+
const received = receiveMessageOnPort(this.port);
|
|
234
|
+
if (received) {
|
|
235
|
+
const reply = received.message;
|
|
236
|
+
if (reply.id !== id)
|
|
237
|
+
continue;
|
|
238
|
+
if (pgNow() >= deadline)
|
|
239
|
+
break;
|
|
240
|
+
if (!reply.ok) {
|
|
241
|
+
if (reply.fatal)
|
|
242
|
+
this.abandon();
|
|
243
|
+
const error = new Error(reply.error);
|
|
244
|
+
error.code = reply.code;
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
248
|
+
}
|
|
249
|
+
const remaining = deadline - pgNow();
|
|
250
|
+
if (remaining <= 0)
|
|
251
|
+
break;
|
|
252
|
+
if (Atomics.load(signal, 0) === 0)
|
|
253
|
+
Atomics.wait(signal, 0, 0, Math.min(remaining, 10));
|
|
254
|
+
else
|
|
255
|
+
Atomics.wait(this.sleeper, 0, 0, Math.min(remaining, 1));
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof Error && "code" in error)
|
|
259
|
+
throw error;
|
|
260
|
+
this.abandon();
|
|
261
|
+
throw new Error("PostgreSQL worker unavailable; write outcome may be unknown");
|
|
262
|
+
}
|
|
263
|
+
this.abandon();
|
|
264
|
+
throw new Error("PostgreSQL operation deadline exceeded; write outcome may be unknown");
|
|
265
|
+
}
|
|
266
|
+
query(sql, params) {
|
|
267
|
+
return this.request("query", translateSqliteDates(translateSql(sql, "pg")), translateParams(params));
|
|
268
|
+
}
|
|
269
|
+
run(sql, ...params) {
|
|
270
|
+
const result = this.query(sql, params);
|
|
271
|
+
return { changes: result.rowCount, lastInsertRowid: result.rows[0]?.["id"] ?? 0 };
|
|
272
|
+
}
|
|
273
|
+
get(sql, ...params) {
|
|
274
|
+
return this.query(sql, params).rows[0] ?? null;
|
|
275
|
+
}
|
|
276
|
+
all(sql, ...params) {
|
|
277
|
+
return this.query(sql, params).rows;
|
|
278
|
+
}
|
|
279
|
+
exec(sql) {
|
|
280
|
+
this.query(sql, []);
|
|
281
|
+
}
|
|
282
|
+
prepare(sql) {
|
|
283
|
+
return {
|
|
284
|
+
run: (...params) => this.run(sql, ...params),
|
|
285
|
+
get: (...params) => this.get(sql, ...params),
|
|
286
|
+
all: (...params) => this.all(sql, ...params),
|
|
287
|
+
finalize: () => {}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
transaction(fn) {
|
|
291
|
+
const outer = this.depth === 0;
|
|
292
|
+
if (outer) {
|
|
293
|
+
this.transactionId = ++this.counter;
|
|
294
|
+
this.transactionDeadline = pgNow() + this.transactionTimeoutMs;
|
|
295
|
+
}
|
|
296
|
+
this.depth++;
|
|
297
|
+
let started = false;
|
|
298
|
+
try {
|
|
299
|
+
this.request(outer ? "begin" : "savepoint");
|
|
300
|
+
started = true;
|
|
301
|
+
const result = fn();
|
|
302
|
+
if (result && typeof result.then === "function") {
|
|
303
|
+
this.abandon();
|
|
304
|
+
Promise.resolve(result).catch(() => {});
|
|
305
|
+
throw new Error("PostgreSQL transaction callback must be synchronous");
|
|
306
|
+
}
|
|
307
|
+
this.request(outer ? "commit" : "release");
|
|
308
|
+
return result;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (started && !this.closed) {
|
|
311
|
+
try {
|
|
312
|
+
this.request(outer ? "rollback" : "rollbackTo");
|
|
313
|
+
} catch {
|
|
314
|
+
this.abandon();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
} finally {
|
|
319
|
+
this.depth--;
|
|
320
|
+
if (outer) {
|
|
321
|
+
this.transactionId = null;
|
|
322
|
+
this.transactionDeadline = Infinity;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
close() {
|
|
327
|
+
this.abandon();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/db/pg-connection.ts
|
|
332
|
+
import pg from "pg";
|
|
333
|
+
import { checkServerIdentity } from "tls";
|
|
334
|
+
function resolvePgConnection(dsn) {
|
|
335
|
+
try {
|
|
336
|
+
const client = new pg.Client({ connectionString: dsn });
|
|
337
|
+
const resolved = client.connectionParameters;
|
|
338
|
+
const ssl = resolved.ssl;
|
|
339
|
+
const host = resolved.host.replace(/^\[|\]$/g, "");
|
|
340
|
+
const tls = ssl ? {
|
|
341
|
+
...typeof ssl === "object" ? ssl : {},
|
|
342
|
+
rejectUnauthorized: true,
|
|
343
|
+
checkServerIdentity: (_servername, cert) => checkServerIdentity(host, cert)
|
|
344
|
+
} : false;
|
|
345
|
+
return { ...resolved, password: resolved.password, ssl: tls };
|
|
346
|
+
} catch {
|
|
347
|
+
throw new Error("PostgreSQL connection configuration is invalid or its TLS files are unreadable");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/db/pg-migrations.ts
|
|
352
|
+
var PG_MIGRATIONS = [
|
|
353
|
+
`CREATE TABLE IF NOT EXISTS cost_centers (
|
|
354
|
+
id TEXT PRIMARY KEY,
|
|
355
|
+
kind TEXT NOT NULL,
|
|
356
|
+
name TEXT NOT NULL,
|
|
357
|
+
repo_path TEXT,
|
|
358
|
+
labels_json TEXT DEFAULT '{}',
|
|
359
|
+
created_at TEXT NOT NULL,
|
|
360
|
+
updated_at TEXT NOT NULL
|
|
361
|
+
)`,
|
|
362
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
363
|
+
id TEXT PRIMARY KEY,
|
|
364
|
+
agent TEXT NOT NULL,
|
|
365
|
+
session_id TEXT NOT NULL,
|
|
366
|
+
model TEXT NOT NULL,
|
|
367
|
+
input_tokens INTEGER DEFAULT 0,
|
|
368
|
+
output_tokens INTEGER DEFAULT 0,
|
|
369
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
370
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
371
|
+
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
372
|
+
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
373
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
374
|
+
duration_ms INTEGER DEFAULT 0,
|
|
375
|
+
timestamp TEXT NOT NULL,
|
|
376
|
+
source_request_id TEXT,
|
|
377
|
+
machine_id TEXT DEFAULT '',
|
|
378
|
+
cost_center_id TEXT,
|
|
379
|
+
account_key TEXT DEFAULT '',
|
|
380
|
+
account_tool TEXT DEFAULT '',
|
|
381
|
+
account_name TEXT DEFAULT '',
|
|
382
|
+
account_email TEXT DEFAULT '',
|
|
383
|
+
account_source TEXT DEFAULT ''
|
|
384
|
+
)`,
|
|
385
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
386
|
+
id TEXT PRIMARY KEY,
|
|
387
|
+
agent TEXT NOT NULL,
|
|
388
|
+
project_path TEXT DEFAULT '',
|
|
389
|
+
project_name TEXT DEFAULT '',
|
|
390
|
+
started_at TEXT NOT NULL,
|
|
391
|
+
ended_at TEXT,
|
|
392
|
+
total_cost_usd REAL DEFAULT 0,
|
|
393
|
+
total_tokens INTEGER DEFAULT 0,
|
|
394
|
+
request_count INTEGER DEFAULT 0,
|
|
395
|
+
machine_id TEXT DEFAULT '',
|
|
396
|
+
cost_center_id TEXT,
|
|
397
|
+
account_key TEXT DEFAULT '',
|
|
398
|
+
account_tool TEXT DEFAULT '',
|
|
399
|
+
account_name TEXT DEFAULT '',
|
|
400
|
+
account_email TEXT DEFAULT '',
|
|
401
|
+
account_source TEXT DEFAULT ''
|
|
402
|
+
)`,
|
|
403
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
404
|
+
id TEXT PRIMARY KEY,
|
|
405
|
+
path TEXT UNIQUE NOT NULL,
|
|
406
|
+
name TEXT NOT NULL,
|
|
407
|
+
description TEXT,
|
|
408
|
+
tags TEXT DEFAULT '[]',
|
|
409
|
+
created_at TEXT NOT NULL
|
|
410
|
+
)`,
|
|
411
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
412
|
+
id TEXT PRIMARY KEY,
|
|
413
|
+
project_path TEXT,
|
|
414
|
+
agent TEXT,
|
|
415
|
+
cost_center_id TEXT,
|
|
416
|
+
period TEXT NOT NULL,
|
|
417
|
+
limit_usd REAL NOT NULL,
|
|
418
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
419
|
+
created_at TEXT NOT NULL,
|
|
420
|
+
updated_at TEXT NOT NULL
|
|
421
|
+
)`,
|
|
422
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
423
|
+
id TEXT PRIMARY KEY,
|
|
424
|
+
period TEXT NOT NULL,
|
|
425
|
+
project_path TEXT,
|
|
426
|
+
agent TEXT,
|
|
427
|
+
limit_usd REAL NOT NULL,
|
|
428
|
+
created_at TEXT NOT NULL,
|
|
429
|
+
updated_at TEXT NOT NULL
|
|
430
|
+
)`,
|
|
431
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
432
|
+
source TEXT NOT NULL,
|
|
433
|
+
key TEXT NOT NULL,
|
|
434
|
+
value TEXT NOT NULL,
|
|
435
|
+
PRIMARY KEY (source, key)
|
|
436
|
+
)`,
|
|
437
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
438
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
439
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
440
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
441
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
442
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
443
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
444
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
445
|
+
`CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind)`,
|
|
446
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
447
|
+
model TEXT PRIMARY KEY,
|
|
448
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
449
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
450
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
451
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
452
|
+
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
453
|
+
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
454
|
+
updated_at TEXT NOT NULL
|
|
455
|
+
)`,
|
|
456
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
457
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
458
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
459
|
+
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
460
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
461
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
462
|
+
message TEXT NOT NULL,
|
|
463
|
+
email TEXT,
|
|
464
|
+
category TEXT DEFAULT 'general',
|
|
465
|
+
version TEXT,
|
|
466
|
+
machine_id TEXT,
|
|
467
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
468
|
+
)`,
|
|
469
|
+
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
470
|
+
date TEXT NOT NULL,
|
|
471
|
+
provider TEXT NOT NULL,
|
|
472
|
+
description TEXT DEFAULT '',
|
|
473
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
474
|
+
updated_at TEXT NOT NULL,
|
|
475
|
+
PRIMARY KEY (date, provider, description)
|
|
476
|
+
)`,
|
|
477
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
478
|
+
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
479
|
+
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
480
|
+
id TEXT PRIMARY KEY,
|
|
481
|
+
agent TEXT,
|
|
482
|
+
provider TEXT NOT NULL,
|
|
483
|
+
plan TEXT NOT NULL,
|
|
484
|
+
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
485
|
+
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
486
|
+
billing_cycle_start TEXT,
|
|
487
|
+
reset_policy TEXT DEFAULT 'monthly',
|
|
488
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
489
|
+
created_at TEXT NOT NULL,
|
|
490
|
+
updated_at TEXT NOT NULL
|
|
491
|
+
)`,
|
|
492
|
+
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
493
|
+
id TEXT PRIMARY KEY,
|
|
494
|
+
agent TEXT NOT NULL,
|
|
495
|
+
date TEXT NOT NULL,
|
|
496
|
+
metric TEXT NOT NULL,
|
|
497
|
+
value REAL NOT NULL DEFAULT 0,
|
|
498
|
+
unit TEXT DEFAULT '',
|
|
499
|
+
machine_id TEXT DEFAULT '',
|
|
500
|
+
updated_at TEXT NOT NULL
|
|
501
|
+
)`,
|
|
502
|
+
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
503
|
+
date TEXT NOT NULL,
|
|
504
|
+
agent TEXT DEFAULT '',
|
|
505
|
+
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
506
|
+
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
507
|
+
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
508
|
+
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
509
|
+
saved_usd REAL NOT NULL DEFAULT 0,
|
|
510
|
+
updated_at TEXT NOT NULL,
|
|
511
|
+
PRIMARY KEY (date, agent)
|
|
512
|
+
)`,
|
|
513
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
514
|
+
machine_id TEXT PRIMARY KEY,
|
|
515
|
+
hostname TEXT NOT NULL,
|
|
516
|
+
last_seen_at TEXT,
|
|
517
|
+
last_push_at TEXT,
|
|
518
|
+
last_pull_at TEXT,
|
|
519
|
+
economy_version TEXT,
|
|
520
|
+
updated_at TEXT NOT NULL
|
|
521
|
+
)`,
|
|
522
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
523
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
524
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
525
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
526
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
527
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
528
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
529
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
530
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
531
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
532
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
533
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
534
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
535
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
536
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
537
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
538
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
539
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
540
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
541
|
+
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
542
|
+
`ALTER TABLE cost_centers ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
543
|
+
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
544
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
545
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
546
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`,
|
|
547
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id)`,
|
|
548
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id)`,
|
|
549
|
+
`CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id)`,
|
|
550
|
+
`ALTER TABLE requests ALTER COLUMN input_tokens TYPE BIGINT`,
|
|
551
|
+
`ALTER TABLE requests ALTER COLUMN output_tokens TYPE BIGINT`,
|
|
552
|
+
`ALTER TABLE requests ALTER COLUMN cache_read_tokens TYPE BIGINT`,
|
|
553
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_tokens TYPE BIGINT`,
|
|
554
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_5m_tokens TYPE BIGINT`,
|
|
555
|
+
`ALTER TABLE requests ALTER COLUMN cache_create_1h_tokens TYPE BIGINT`,
|
|
556
|
+
`ALTER TABLE requests ALTER COLUMN duration_ms TYPE BIGINT`,
|
|
557
|
+
`ALTER TABLE sessions ALTER COLUMN total_tokens TYPE BIGINT`,
|
|
558
|
+
`ALTER TABLE sessions ALTER COLUMN request_count TYPE BIGINT`,
|
|
559
|
+
`ALTER TABLE requests ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
|
|
560
|
+
`ALTER TABLE sessions ALTER COLUMN total_cost_usd TYPE DOUBLE PRECISION`,
|
|
561
|
+
`ALTER TABLE budgets ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
|
|
562
|
+
`ALTER TABLE goals ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
|
|
563
|
+
`ALTER TABLE model_pricing ALTER COLUMN input_per_1m TYPE DOUBLE PRECISION`,
|
|
564
|
+
`ALTER TABLE model_pricing ALTER COLUMN output_per_1m TYPE DOUBLE PRECISION`,
|
|
565
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_read_per_1m TYPE DOUBLE PRECISION`,
|
|
566
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_write_per_1m TYPE DOUBLE PRECISION`,
|
|
567
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_write_1h_per_1m TYPE DOUBLE PRECISION`,
|
|
568
|
+
`ALTER TABLE model_pricing ALTER COLUMN cache_storage_per_1m_hour TYPE DOUBLE PRECISION`,
|
|
569
|
+
`ALTER TABLE billing_daily ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
|
|
570
|
+
`ALTER TABLE subscriptions ALTER COLUMN monthly_fee_usd TYPE DOUBLE PRECISION`,
|
|
571
|
+
`ALTER TABLE subscriptions ALTER COLUMN included_usage_usd TYPE DOUBLE PRECISION`,
|
|
572
|
+
`ALTER TABLE usage_snapshots ALTER COLUMN value TYPE DOUBLE PRECISION`,
|
|
573
|
+
`ALTER TABLE savings_daily ALTER COLUMN api_equivalent_usd TYPE DOUBLE PRECISION`,
|
|
574
|
+
`ALTER TABLE savings_daily ALTER COLUMN subscription_fee_usd TYPE DOUBLE PRECISION`,
|
|
575
|
+
`ALTER TABLE savings_daily ALTER COLUMN included_consumed_usd TYPE DOUBLE PRECISION`,
|
|
576
|
+
`ALTER TABLE savings_daily ALTER COLUMN on_demand_usd TYPE DOUBLE PRECISION`,
|
|
577
|
+
`ALTER TABLE savings_daily ALTER COLUMN saved_usd TYPE DOUBLE PRECISION`,
|
|
578
|
+
INGEST_SCHEMA_SQL,
|
|
579
|
+
...OBSERVATION_PG_MIGRATIONS,
|
|
580
|
+
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
|
|
581
|
+
CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
|
|
582
|
+
LEGACY_PG_SQL
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
// src/db/cloud.ts
|
|
586
|
+
function getCloudDatabaseUrl(env = process.env) {
|
|
587
|
+
return env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || env["ECONOMY_DATABASE_URL"]?.trim() || env["DATABASE_URL"]?.trim() || undefined;
|
|
588
|
+
}
|
|
589
|
+
function resolveEconomyServerBackend(env = process.env) {
|
|
590
|
+
return getCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
|
|
591
|
+
}
|
|
592
|
+
function isPostgresBackend(env = process.env) {
|
|
593
|
+
return resolveEconomyServerBackend(env) === "postgresql";
|
|
594
|
+
}
|
|
595
|
+
function resolveSigningSecret() {
|
|
596
|
+
return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
|
|
597
|
+
}
|
|
598
|
+
function createCloudPool(dsn = getCloudDatabaseUrl(), options = {}) {
|
|
599
|
+
if (!dsn)
|
|
600
|
+
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
601
|
+
const pool = new pg2.Pool({ ...resolvePgConnection(dsn), max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4, ...options });
|
|
602
|
+
pool.on("error", () => {
|
|
603
|
+
console.error(JSON.stringify({ evt: "pg_pool_error" }));
|
|
604
|
+
});
|
|
605
|
+
return pool;
|
|
606
|
+
}
|
|
607
|
+
function authClientFromPool(pool) {
|
|
608
|
+
return {
|
|
609
|
+
async many(sql, params = []) {
|
|
610
|
+
const res = await pool.query(sql, params);
|
|
611
|
+
return res.rows;
|
|
612
|
+
},
|
|
613
|
+
async get(sql, params = []) {
|
|
614
|
+
const res = await pool.query(sql, params);
|
|
615
|
+
return res.rows[0] ?? null;
|
|
616
|
+
},
|
|
617
|
+
async execute(sql, params = []) {
|
|
618
|
+
await pool.query(sql, params);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/server/serve.ts
|
|
624
|
+
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
625
|
+
|
|
626
|
+
// src/openapi.ts
|
|
627
|
+
var openApiSpec = {
|
|
628
|
+
openapi: "3.0.3",
|
|
629
|
+
info: {
|
|
630
|
+
title: "@hasna/economy API",
|
|
631
|
+
version: "0.0.0",
|
|
632
|
+
description: "AI coding cost tracker \u2014 deployed economy control plane. The server data backend is sqlite | postgresql, selected by the presence of a database URL; with postgresql the serve process reads AND writes PostgreSQL directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open. Explicitly configured human access additionally accepts short-lived Economy identity tokens with current session/grant checks and a fixed report-store tenant/principal binding."
|
|
633
|
+
},
|
|
634
|
+
servers: [
|
|
635
|
+
{
|
|
636
|
+
url: "http://localhost:3456",
|
|
637
|
+
description: "local default; set your own URL when deploying"
|
|
638
|
+
}
|
|
639
|
+
],
|
|
640
|
+
security: [
|
|
641
|
+
{
|
|
642
|
+
apiKey: []
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
bearer: []
|
|
646
|
+
}
|
|
647
|
+
],
|
|
648
|
+
components: {
|
|
649
|
+
securitySchemes: {
|
|
650
|
+
apiKey: {
|
|
651
|
+
type: "apiKey",
|
|
652
|
+
in: "header",
|
|
653
|
+
name: "x-api-key"
|
|
654
|
+
},
|
|
655
|
+
bearer: {
|
|
656
|
+
type: "http",
|
|
657
|
+
scheme: "bearer"
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
schemas: {
|
|
661
|
+
Envelope: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
data: {},
|
|
665
|
+
meta: {
|
|
666
|
+
type: "object",
|
|
667
|
+
additionalProperties: true
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
required: [
|
|
671
|
+
"data"
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
Health: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
status: {
|
|
678
|
+
type: "string",
|
|
679
|
+
enum: [
|
|
680
|
+
"ok",
|
|
681
|
+
"degraded",
|
|
682
|
+
"unavailable"
|
|
683
|
+
]
|
|
684
|
+
},
|
|
685
|
+
version: {
|
|
686
|
+
type: "string"
|
|
687
|
+
},
|
|
688
|
+
backend: {
|
|
689
|
+
type: "string",
|
|
690
|
+
enum: [
|
|
691
|
+
"sqlite",
|
|
692
|
+
"postgresql"
|
|
693
|
+
]
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
required: [
|
|
697
|
+
"status",
|
|
698
|
+
"version",
|
|
699
|
+
"backend"
|
|
700
|
+
],
|
|
701
|
+
additionalProperties: false
|
|
702
|
+
},
|
|
703
|
+
Ready: {
|
|
704
|
+
type: "object",
|
|
705
|
+
properties: {
|
|
706
|
+
ready: {
|
|
707
|
+
type: "boolean"
|
|
708
|
+
},
|
|
709
|
+
reason: {
|
|
710
|
+
type: "string"
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
required: [
|
|
714
|
+
"ready"
|
|
715
|
+
],
|
|
716
|
+
additionalProperties: false
|
|
717
|
+
},
|
|
718
|
+
Version: {
|
|
719
|
+
type: "object",
|
|
720
|
+
properties: {
|
|
721
|
+
version: {
|
|
722
|
+
type: "string"
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
required: [
|
|
726
|
+
"version"
|
|
727
|
+
],
|
|
728
|
+
additionalProperties: false
|
|
729
|
+
},
|
|
730
|
+
Error: {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: {
|
|
733
|
+
error: {
|
|
734
|
+
type: "string"
|
|
735
|
+
},
|
|
736
|
+
message: {
|
|
737
|
+
type: "string"
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
required: [
|
|
741
|
+
"error"
|
|
742
|
+
]
|
|
743
|
+
},
|
|
744
|
+
MutationOk: {
|
|
745
|
+
type: "object",
|
|
746
|
+
properties: {
|
|
747
|
+
ok: {
|
|
748
|
+
type: "boolean"
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
CostSummary: {
|
|
753
|
+
type: "object",
|
|
754
|
+
properties: {
|
|
755
|
+
total_usd: {
|
|
756
|
+
type: "number",
|
|
757
|
+
minimum: 0,
|
|
758
|
+
description: "Compatibility subtotal of known stored usage values, including separately reported session-only values. Not a paid bill."
|
|
759
|
+
},
|
|
760
|
+
complete_total_usd: {
|
|
761
|
+
type: "number",
|
|
762
|
+
nullable: true,
|
|
763
|
+
minimum: 0
|
|
764
|
+
},
|
|
765
|
+
basis: {
|
|
766
|
+
type: "string",
|
|
767
|
+
enum: [
|
|
768
|
+
"known_usage_value_subtotal"
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
completeness: {
|
|
772
|
+
type: "string",
|
|
773
|
+
enum: [
|
|
774
|
+
"complete",
|
|
775
|
+
"partial",
|
|
776
|
+
"unavailable",
|
|
777
|
+
"no_records"
|
|
778
|
+
]
|
|
779
|
+
},
|
|
780
|
+
coverage_scope: {
|
|
781
|
+
type: "string",
|
|
782
|
+
enum: [
|
|
783
|
+
"selected_stored_records"
|
|
784
|
+
]
|
|
785
|
+
},
|
|
786
|
+
source_coverage: {
|
|
787
|
+
type: "string",
|
|
788
|
+
enum: [
|
|
789
|
+
"not_established"
|
|
790
|
+
]
|
|
791
|
+
},
|
|
792
|
+
period_bounds: {
|
|
793
|
+
$ref: "#/components/schemas/CalendarPeriodBounds"
|
|
794
|
+
},
|
|
795
|
+
period: {
|
|
796
|
+
type: "string",
|
|
797
|
+
enum: [
|
|
798
|
+
"today",
|
|
799
|
+
"yesterday",
|
|
800
|
+
"week",
|
|
801
|
+
"month",
|
|
802
|
+
"year",
|
|
803
|
+
"all"
|
|
804
|
+
]
|
|
805
|
+
},
|
|
806
|
+
recorded_metered_api_usd: {
|
|
807
|
+
type: "number",
|
|
808
|
+
minimum: 0
|
|
809
|
+
},
|
|
810
|
+
estimated_usage_value_usd: {
|
|
811
|
+
type: "number",
|
|
812
|
+
minimum: 0
|
|
813
|
+
},
|
|
814
|
+
subscription_included_value_usd: {
|
|
815
|
+
type: "number",
|
|
816
|
+
minimum: 0
|
|
817
|
+
},
|
|
818
|
+
session_only_value_usd: {
|
|
819
|
+
type: "number",
|
|
820
|
+
minimum: 0
|
|
821
|
+
},
|
|
822
|
+
request_rows: {
|
|
823
|
+
type: "integer",
|
|
824
|
+
minimum: 0
|
|
825
|
+
},
|
|
826
|
+
unknown_price_requests: {
|
|
827
|
+
type: "integer",
|
|
828
|
+
minimum: 0
|
|
829
|
+
},
|
|
830
|
+
unknown_basis_requests: {
|
|
831
|
+
type: "integer",
|
|
832
|
+
minimum: 0
|
|
833
|
+
},
|
|
834
|
+
session_only_sessions: {
|
|
835
|
+
type: "integer",
|
|
836
|
+
minimum: 0
|
|
837
|
+
},
|
|
838
|
+
sessions: {
|
|
839
|
+
type: "integer",
|
|
840
|
+
minimum: 0
|
|
841
|
+
},
|
|
842
|
+
requests: {
|
|
843
|
+
type: "integer",
|
|
844
|
+
minimum: 0
|
|
845
|
+
},
|
|
846
|
+
tokens: {
|
|
847
|
+
type: "integer",
|
|
848
|
+
minimum: 0
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
required: [
|
|
852
|
+
"total_usd",
|
|
853
|
+
"complete_total_usd",
|
|
854
|
+
"basis",
|
|
855
|
+
"completeness",
|
|
856
|
+
"coverage_scope",
|
|
857
|
+
"source_coverage",
|
|
858
|
+
"period_bounds",
|
|
859
|
+
"period",
|
|
860
|
+
"recorded_metered_api_usd",
|
|
861
|
+
"estimated_usage_value_usd",
|
|
862
|
+
"subscription_included_value_usd",
|
|
863
|
+
"session_only_value_usd",
|
|
864
|
+
"request_rows",
|
|
865
|
+
"unknown_price_requests",
|
|
866
|
+
"unknown_basis_requests",
|
|
867
|
+
"session_only_sessions",
|
|
868
|
+
"sessions",
|
|
869
|
+
"requests",
|
|
870
|
+
"tokens"
|
|
871
|
+
],
|
|
872
|
+
description: "Complete applies only to selected stored records. Unknown prices or session-only records make complete_total_usd null. Source coverage is never established by this aggregate."
|
|
873
|
+
},
|
|
874
|
+
Session: {
|
|
875
|
+
type: "object",
|
|
876
|
+
properties: {
|
|
877
|
+
id: {
|
|
878
|
+
type: "string"
|
|
879
|
+
},
|
|
880
|
+
agent: {
|
|
881
|
+
type: "string"
|
|
882
|
+
},
|
|
883
|
+
project_path: {
|
|
884
|
+
type: "string"
|
|
885
|
+
},
|
|
886
|
+
project_name: {
|
|
887
|
+
type: "string"
|
|
888
|
+
},
|
|
889
|
+
started_at: {
|
|
890
|
+
type: "string"
|
|
891
|
+
},
|
|
892
|
+
ended_at: {
|
|
893
|
+
type: "string",
|
|
894
|
+
nullable: true
|
|
895
|
+
},
|
|
896
|
+
total_cost_usd: {
|
|
897
|
+
type: "number"
|
|
898
|
+
},
|
|
899
|
+
total_tokens: {
|
|
900
|
+
type: "number"
|
|
901
|
+
},
|
|
902
|
+
request_count: {
|
|
903
|
+
type: "number"
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
additionalProperties: true
|
|
907
|
+
},
|
|
908
|
+
ModelBreakdown: {
|
|
909
|
+
type: "object",
|
|
910
|
+
additionalProperties: true
|
|
911
|
+
},
|
|
912
|
+
ProjectBreakdown: {
|
|
913
|
+
type: "object",
|
|
914
|
+
additionalProperties: true
|
|
915
|
+
},
|
|
916
|
+
AgentBreakdown: {
|
|
917
|
+
type: "object",
|
|
918
|
+
additionalProperties: true
|
|
919
|
+
},
|
|
920
|
+
AccountBreakdown: {
|
|
921
|
+
type: "object",
|
|
922
|
+
additionalProperties: true
|
|
923
|
+
},
|
|
924
|
+
DailyPoint: {
|
|
925
|
+
type: "object",
|
|
926
|
+
additionalProperties: true
|
|
927
|
+
},
|
|
928
|
+
BudgetStatus: {
|
|
929
|
+
type: "object",
|
|
930
|
+
properties: {
|
|
931
|
+
id: {
|
|
932
|
+
type: "string"
|
|
933
|
+
},
|
|
934
|
+
limit_usd: {
|
|
935
|
+
type: "number"
|
|
936
|
+
},
|
|
937
|
+
period: {
|
|
938
|
+
type: "string"
|
|
939
|
+
},
|
|
940
|
+
spent_usd: {
|
|
941
|
+
type: "number"
|
|
942
|
+
},
|
|
943
|
+
metric: {
|
|
944
|
+
type: "string",
|
|
945
|
+
enum: [
|
|
946
|
+
"legacy_request_cost_usd",
|
|
947
|
+
"metered_api_usd"
|
|
948
|
+
]
|
|
949
|
+
},
|
|
950
|
+
current_spend_usd: {
|
|
951
|
+
type: "number"
|
|
952
|
+
},
|
|
953
|
+
percent_used: {
|
|
954
|
+
type: "number"
|
|
955
|
+
},
|
|
956
|
+
is_over_limit: {
|
|
957
|
+
type: "boolean"
|
|
958
|
+
},
|
|
959
|
+
is_over_alert: {
|
|
960
|
+
type: "boolean"
|
|
961
|
+
},
|
|
962
|
+
period_bounds: {
|
|
963
|
+
type: "object",
|
|
964
|
+
required: [
|
|
965
|
+
"time_zone",
|
|
966
|
+
"start_date",
|
|
967
|
+
"end_date_exclusive"
|
|
968
|
+
],
|
|
969
|
+
properties: {
|
|
970
|
+
time_zone: {
|
|
971
|
+
type: "string",
|
|
972
|
+
enum: [
|
|
973
|
+
"UTC"
|
|
974
|
+
]
|
|
975
|
+
},
|
|
976
|
+
start_date: {
|
|
977
|
+
type: "string",
|
|
978
|
+
format: "date"
|
|
979
|
+
},
|
|
980
|
+
end_date_exclusive: {
|
|
981
|
+
type: "string",
|
|
982
|
+
format: "date"
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
additionalProperties: true
|
|
988
|
+
},
|
|
989
|
+
CreateBudgetInput: {
|
|
990
|
+
type: "object",
|
|
991
|
+
properties: {
|
|
992
|
+
limit_usd: {
|
|
993
|
+
type: "number"
|
|
994
|
+
},
|
|
995
|
+
alert_at_percent: {
|
|
996
|
+
type: "number"
|
|
997
|
+
},
|
|
998
|
+
period: {
|
|
999
|
+
type: "string",
|
|
1000
|
+
enum: [
|
|
1001
|
+
"daily",
|
|
1002
|
+
"weekly",
|
|
1003
|
+
"monthly",
|
|
1004
|
+
"day",
|
|
1005
|
+
"week",
|
|
1006
|
+
"month"
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
project_path: {
|
|
1010
|
+
type: "string",
|
|
1011
|
+
nullable: true
|
|
1012
|
+
},
|
|
1013
|
+
agent: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
nullable: true
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
required: [
|
|
1019
|
+
"limit_usd"
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
GoalStatus: {
|
|
1023
|
+
type: "object",
|
|
1024
|
+
additionalProperties: true
|
|
1025
|
+
},
|
|
1026
|
+
CreateGoalInput: {
|
|
1027
|
+
type: "object",
|
|
1028
|
+
properties: {
|
|
1029
|
+
period: {
|
|
1030
|
+
type: "string",
|
|
1031
|
+
enum: [
|
|
1032
|
+
"day",
|
|
1033
|
+
"week",
|
|
1034
|
+
"month",
|
|
1035
|
+
"year"
|
|
1036
|
+
]
|
|
1037
|
+
},
|
|
1038
|
+
limit_usd: {
|
|
1039
|
+
type: "number"
|
|
1040
|
+
},
|
|
1041
|
+
project_path: {
|
|
1042
|
+
type: "string",
|
|
1043
|
+
nullable: true
|
|
1044
|
+
},
|
|
1045
|
+
agent: {
|
|
1046
|
+
type: "string",
|
|
1047
|
+
nullable: true
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
required: [
|
|
1051
|
+
"limit_usd"
|
|
1052
|
+
]
|
|
1053
|
+
},
|
|
1054
|
+
ModelPricing: {
|
|
1055
|
+
type: "object",
|
|
1056
|
+
additionalProperties: true
|
|
1057
|
+
},
|
|
1058
|
+
CreatePricingInput: {
|
|
1059
|
+
type: "object",
|
|
1060
|
+
properties: {
|
|
1061
|
+
model: {
|
|
1062
|
+
type: "string"
|
|
1063
|
+
},
|
|
1064
|
+
input_per_1m: {
|
|
1065
|
+
type: "number"
|
|
1066
|
+
},
|
|
1067
|
+
output_per_1m: {
|
|
1068
|
+
type: "number"
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
required: [
|
|
1072
|
+
"model",
|
|
1073
|
+
"input_per_1m",
|
|
1074
|
+
"output_per_1m"
|
|
1075
|
+
]
|
|
1076
|
+
},
|
|
1077
|
+
Subscription: {
|
|
1078
|
+
type: "object",
|
|
1079
|
+
properties: {
|
|
1080
|
+
id: {
|
|
1081
|
+
type: "string"
|
|
1082
|
+
},
|
|
1083
|
+
provider: {
|
|
1084
|
+
type: "string"
|
|
1085
|
+
},
|
|
1086
|
+
plan: {
|
|
1087
|
+
type: "string"
|
|
1088
|
+
},
|
|
1089
|
+
agent: {
|
|
1090
|
+
type: "string",
|
|
1091
|
+
nullable: true
|
|
1092
|
+
},
|
|
1093
|
+
account_key: {
|
|
1094
|
+
type: "string"
|
|
1095
|
+
},
|
|
1096
|
+
monthly_fee_usd: {
|
|
1097
|
+
type: "number",
|
|
1098
|
+
minimum: 0
|
|
1099
|
+
},
|
|
1100
|
+
included_usage_usd: {
|
|
1101
|
+
type: "number",
|
|
1102
|
+
minimum: 0
|
|
1103
|
+
},
|
|
1104
|
+
fee_basis: {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
enum: [
|
|
1107
|
+
"unknown",
|
|
1108
|
+
"observed",
|
|
1109
|
+
"configured"
|
|
1110
|
+
]
|
|
1111
|
+
},
|
|
1112
|
+
included_basis: {
|
|
1113
|
+
type: "string",
|
|
1114
|
+
enum: [
|
|
1115
|
+
"unknown",
|
|
1116
|
+
"observed",
|
|
1117
|
+
"configured"
|
|
1118
|
+
]
|
|
1119
|
+
},
|
|
1120
|
+
observed_at: {
|
|
1121
|
+
type: "string",
|
|
1122
|
+
format: "date-time",
|
|
1123
|
+
nullable: true
|
|
1124
|
+
},
|
|
1125
|
+
observed_station: {
|
|
1126
|
+
type: "string",
|
|
1127
|
+
nullable: true
|
|
1128
|
+
},
|
|
1129
|
+
billing_cycle_start: {
|
|
1130
|
+
type: "string",
|
|
1131
|
+
format: "date",
|
|
1132
|
+
nullable: true
|
|
1133
|
+
},
|
|
1134
|
+
reset_policy: {
|
|
1135
|
+
type: "string"
|
|
1136
|
+
},
|
|
1137
|
+
active: {
|
|
1138
|
+
type: "integer",
|
|
1139
|
+
enum: [
|
|
1140
|
+
0,
|
|
1141
|
+
1
|
|
1142
|
+
]
|
|
1143
|
+
},
|
|
1144
|
+
created_at: {
|
|
1145
|
+
type: "string",
|
|
1146
|
+
format: "date-time",
|
|
1147
|
+
nullable: true
|
|
1148
|
+
},
|
|
1149
|
+
updated_at: {
|
|
1150
|
+
type: "string",
|
|
1151
|
+
format: "date-time",
|
|
1152
|
+
nullable: true
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
description: "Account identity is SHA-256 of JSON [provider, account_key]. Legacy numeric amounts remain stored but unknown basis is not a verified fee or free plan."
|
|
1156
|
+
},
|
|
1157
|
+
CreateSubscriptionInput: {
|
|
1158
|
+
type: "object",
|
|
1159
|
+
properties: {
|
|
1160
|
+
id: {
|
|
1161
|
+
type: "string"
|
|
1162
|
+
},
|
|
1163
|
+
provider: {
|
|
1164
|
+
type: "string"
|
|
1165
|
+
},
|
|
1166
|
+
plan: {
|
|
1167
|
+
type: "string"
|
|
1168
|
+
},
|
|
1169
|
+
agent: {
|
|
1170
|
+
type: "string",
|
|
1171
|
+
nullable: true
|
|
1172
|
+
},
|
|
1173
|
+
account_key: {
|
|
1174
|
+
type: "string"
|
|
1175
|
+
},
|
|
1176
|
+
monthly_fee_usd: {
|
|
1177
|
+
type: "number",
|
|
1178
|
+
minimum: 0
|
|
1179
|
+
},
|
|
1180
|
+
included_usage_usd: {
|
|
1181
|
+
type: "number",
|
|
1182
|
+
minimum: 0
|
|
1183
|
+
},
|
|
1184
|
+
fee_basis: {
|
|
1185
|
+
type: "string",
|
|
1186
|
+
enum: [
|
|
1187
|
+
"unknown",
|
|
1188
|
+
"observed",
|
|
1189
|
+
"configured"
|
|
1190
|
+
]
|
|
1191
|
+
},
|
|
1192
|
+
included_basis: {
|
|
1193
|
+
type: "string",
|
|
1194
|
+
enum: [
|
|
1195
|
+
"unknown",
|
|
1196
|
+
"observed",
|
|
1197
|
+
"configured"
|
|
1198
|
+
]
|
|
1199
|
+
},
|
|
1200
|
+
observed_at: {
|
|
1201
|
+
type: "string",
|
|
1202
|
+
format: "date-time",
|
|
1203
|
+
nullable: true
|
|
1204
|
+
},
|
|
1205
|
+
observed_station: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
nullable: true
|
|
1208
|
+
},
|
|
1209
|
+
billing_cycle_start: {
|
|
1210
|
+
type: "string",
|
|
1211
|
+
format: "date",
|
|
1212
|
+
nullable: true
|
|
1213
|
+
},
|
|
1214
|
+
reset_policy: {
|
|
1215
|
+
type: "string"
|
|
1216
|
+
},
|
|
1217
|
+
active: {
|
|
1218
|
+
type: "integer",
|
|
1219
|
+
enum: [
|
|
1220
|
+
0,
|
|
1221
|
+
1
|
|
1222
|
+
]
|
|
1223
|
+
},
|
|
1224
|
+
created_at: {
|
|
1225
|
+
type: "string",
|
|
1226
|
+
format: "date-time",
|
|
1227
|
+
nullable: true
|
|
1228
|
+
},
|
|
1229
|
+
updated_at: {
|
|
1230
|
+
type: "string",
|
|
1231
|
+
format: "date-time",
|
|
1232
|
+
nullable: true
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
required: [
|
|
1236
|
+
"provider",
|
|
1237
|
+
"plan"
|
|
1238
|
+
],
|
|
1239
|
+
description: "Known monetary basis requires account_key and observed_at; server supplies observed_at when account_key is provided. Omitted basis is unknown. The CLI/SDK configure explicit amounts with configured basis and require account_key. Null numeric amounts are refused."
|
|
1240
|
+
},
|
|
1241
|
+
Project: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
additionalProperties: true
|
|
1244
|
+
},
|
|
1245
|
+
CreateProjectInput: {
|
|
1246
|
+
type: "object",
|
|
1247
|
+
properties: {
|
|
1248
|
+
path: {
|
|
1249
|
+
type: "string"
|
|
1250
|
+
},
|
|
1251
|
+
name: {
|
|
1252
|
+
type: "string"
|
|
1253
|
+
},
|
|
1254
|
+
description: {
|
|
1255
|
+
type: "string"
|
|
1256
|
+
},
|
|
1257
|
+
tags: {
|
|
1258
|
+
type: "array",
|
|
1259
|
+
items: {
|
|
1260
|
+
type: "string"
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
required: [
|
|
1265
|
+
"path"
|
|
1266
|
+
]
|
|
1267
|
+
},
|
|
1268
|
+
MachineInfo: {
|
|
1269
|
+
type: "object",
|
|
1270
|
+
additionalProperties: true
|
|
1271
|
+
},
|
|
1272
|
+
BillingSummary: {
|
|
1273
|
+
type: "object",
|
|
1274
|
+
properties: {
|
|
1275
|
+
total_usd: {
|
|
1276
|
+
type: "number",
|
|
1277
|
+
description: "Compatibility subtotal of selected imported provider rows, including recorded credits."
|
|
1278
|
+
},
|
|
1279
|
+
recorded_total_usd: {
|
|
1280
|
+
type: "number",
|
|
1281
|
+
nullable: true
|
|
1282
|
+
},
|
|
1283
|
+
complete_total_usd: {
|
|
1284
|
+
type: "number",
|
|
1285
|
+
nullable: true,
|
|
1286
|
+
enum: [
|
|
1287
|
+
null
|
|
1288
|
+
]
|
|
1289
|
+
},
|
|
1290
|
+
basis: {
|
|
1291
|
+
type: "string",
|
|
1292
|
+
enum: [
|
|
1293
|
+
"recorded_provider_rows_subtotal"
|
|
1294
|
+
]
|
|
1295
|
+
},
|
|
1296
|
+
status: {
|
|
1297
|
+
type: "string",
|
|
1298
|
+
enum: [
|
|
1299
|
+
"observed",
|
|
1300
|
+
"no_records"
|
|
1301
|
+
]
|
|
1302
|
+
},
|
|
1303
|
+
coverage_scope: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
enum: [
|
|
1306
|
+
"selected_stored_records"
|
|
1307
|
+
]
|
|
1308
|
+
},
|
|
1309
|
+
source_coverage: {
|
|
1310
|
+
type: "string",
|
|
1311
|
+
enum: [
|
|
1312
|
+
"not_established"
|
|
1313
|
+
]
|
|
1314
|
+
},
|
|
1315
|
+
period_bounds: {
|
|
1316
|
+
$ref: "#/components/schemas/CalendarPeriodBounds"
|
|
1317
|
+
},
|
|
1318
|
+
record_count: {
|
|
1319
|
+
type: "integer",
|
|
1320
|
+
minimum: 0
|
|
1321
|
+
},
|
|
1322
|
+
by_provider: {
|
|
1323
|
+
type: "object",
|
|
1324
|
+
additionalProperties: {
|
|
1325
|
+
type: "number"
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
required: [
|
|
1330
|
+
"total_usd",
|
|
1331
|
+
"recorded_total_usd",
|
|
1332
|
+
"complete_total_usd",
|
|
1333
|
+
"basis",
|
|
1334
|
+
"status",
|
|
1335
|
+
"coverage_scope",
|
|
1336
|
+
"source_coverage",
|
|
1337
|
+
"period_bounds",
|
|
1338
|
+
"record_count",
|
|
1339
|
+
"by_provider"
|
|
1340
|
+
],
|
|
1341
|
+
description: "Recorded provider-row subtotal only. Empty records produce null recorded_total_usd; an actual observed zero remains zero. Complete account/invoice/source coverage is unverified."
|
|
1342
|
+
},
|
|
1343
|
+
UsageSnapshot: {
|
|
1344
|
+
type: "object",
|
|
1345
|
+
properties: {
|
|
1346
|
+
id: {
|
|
1347
|
+
type: "string"
|
|
1348
|
+
},
|
|
1349
|
+
agent: {
|
|
1350
|
+
type: "string"
|
|
1351
|
+
},
|
|
1352
|
+
date: {
|
|
1353
|
+
type: "string",
|
|
1354
|
+
format: "date"
|
|
1355
|
+
},
|
|
1356
|
+
metric: {
|
|
1357
|
+
type: "string"
|
|
1358
|
+
},
|
|
1359
|
+
value: {
|
|
1360
|
+
type: "number",
|
|
1361
|
+
minimum: 0
|
|
1362
|
+
},
|
|
1363
|
+
unit: {
|
|
1364
|
+
type: "string"
|
|
1365
|
+
},
|
|
1366
|
+
machine_id: {
|
|
1367
|
+
type: "string"
|
|
1368
|
+
},
|
|
1369
|
+
updated_at: {
|
|
1370
|
+
type: "string",
|
|
1371
|
+
format: "date-time",
|
|
1372
|
+
nullable: true
|
|
1373
|
+
},
|
|
1374
|
+
provider: {
|
|
1375
|
+
type: "string"
|
|
1376
|
+
},
|
|
1377
|
+
account_key: {
|
|
1378
|
+
type: "string"
|
|
1379
|
+
},
|
|
1380
|
+
observed_at: {
|
|
1381
|
+
type: "string",
|
|
1382
|
+
format: "date-time",
|
|
1383
|
+
nullable: true
|
|
1384
|
+
},
|
|
1385
|
+
source_timestamp: {
|
|
1386
|
+
type: "string",
|
|
1387
|
+
format: "date-time",
|
|
1388
|
+
nullable: true
|
|
1389
|
+
},
|
|
1390
|
+
window_start: {
|
|
1391
|
+
type: "string",
|
|
1392
|
+
format: "date-time",
|
|
1393
|
+
nullable: true
|
|
1394
|
+
},
|
|
1395
|
+
window_end: {
|
|
1396
|
+
type: "string",
|
|
1397
|
+
format: "date-time",
|
|
1398
|
+
nullable: true
|
|
1399
|
+
},
|
|
1400
|
+
reset_at: {
|
|
1401
|
+
type: "string",
|
|
1402
|
+
format: "date-time",
|
|
1403
|
+
nullable: true
|
|
1404
|
+
},
|
|
1405
|
+
window_seconds: {
|
|
1406
|
+
type: "integer",
|
|
1407
|
+
minimum: 1,
|
|
1408
|
+
nullable: true
|
|
1409
|
+
},
|
|
1410
|
+
value_kind: {
|
|
1411
|
+
type: "string",
|
|
1412
|
+
enum: [
|
|
1413
|
+
"gauge",
|
|
1414
|
+
"cumulative",
|
|
1415
|
+
"delta",
|
|
1416
|
+
"unknown"
|
|
1417
|
+
]
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
description: "Immutable identified observation: provider/account/station/metric/observed_at. Measurements retain their source units and actual window; a cumulative value is not a request or delta."
|
|
1421
|
+
},
|
|
1422
|
+
ScanReceipt: {
|
|
1423
|
+
type: "object",
|
|
1424
|
+
properties: {
|
|
1425
|
+
id: {
|
|
1426
|
+
type: "string"
|
|
1427
|
+
},
|
|
1428
|
+
machine_id: {
|
|
1429
|
+
type: "string"
|
|
1430
|
+
},
|
|
1431
|
+
source: {
|
|
1432
|
+
type: "string"
|
|
1433
|
+
},
|
|
1434
|
+
attempted_at: {
|
|
1435
|
+
type: "string",
|
|
1436
|
+
format: "date-time"
|
|
1437
|
+
},
|
|
1438
|
+
completed_at: {
|
|
1439
|
+
type: "string",
|
|
1440
|
+
format: "date-time"
|
|
1441
|
+
},
|
|
1442
|
+
status: {
|
|
1443
|
+
type: "string",
|
|
1444
|
+
enum: [
|
|
1445
|
+
"success",
|
|
1446
|
+
"unchanged",
|
|
1447
|
+
"missing",
|
|
1448
|
+
"unsupported",
|
|
1449
|
+
"failed"
|
|
1450
|
+
]
|
|
1451
|
+
},
|
|
1452
|
+
latest_event_at: {
|
|
1453
|
+
type: "string",
|
|
1454
|
+
format: "date-time",
|
|
1455
|
+
nullable: true
|
|
1456
|
+
},
|
|
1457
|
+
parser_version: {
|
|
1458
|
+
type: "string"
|
|
1459
|
+
},
|
|
1460
|
+
error_code: {
|
|
1461
|
+
type: "string",
|
|
1462
|
+
nullable: true
|
|
1463
|
+
},
|
|
1464
|
+
updated_at: {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
format: "date-time",
|
|
1467
|
+
nullable: true
|
|
1468
|
+
}
|
|
1469
|
+
},
|
|
1470
|
+
required: [
|
|
1471
|
+
"id",
|
|
1472
|
+
"machine_id",
|
|
1473
|
+
"source",
|
|
1474
|
+
"attempted_at",
|
|
1475
|
+
"completed_at",
|
|
1476
|
+
"status",
|
|
1477
|
+
"parser_version"
|
|
1478
|
+
],
|
|
1479
|
+
description: "Collection attempt and source-event freshness are separate. Error codes are a fixed authored allowlist, never raw provider responses."
|
|
1480
|
+
},
|
|
1481
|
+
IngestReceipt: {
|
|
1482
|
+
type: "object",
|
|
1483
|
+
required: [
|
|
1484
|
+
"ingested",
|
|
1485
|
+
"total",
|
|
1486
|
+
"stale",
|
|
1487
|
+
"unchanged",
|
|
1488
|
+
"operation_key",
|
|
1489
|
+
"replayed"
|
|
1490
|
+
],
|
|
1491
|
+
properties: {
|
|
1492
|
+
ingested: {
|
|
1493
|
+
type: "object",
|
|
1494
|
+
additionalProperties: {
|
|
1495
|
+
type: "integer",
|
|
1496
|
+
minimum: 0
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
total: {
|
|
1500
|
+
type: "integer",
|
|
1501
|
+
minimum: 0
|
|
1502
|
+
},
|
|
1503
|
+
stale: {
|
|
1504
|
+
type: "integer",
|
|
1505
|
+
minimum: 0
|
|
1506
|
+
},
|
|
1507
|
+
unchanged: {
|
|
1508
|
+
type: "integer",
|
|
1509
|
+
minimum: 0
|
|
1510
|
+
},
|
|
1511
|
+
operation_key: {
|
|
1512
|
+
type: "string",
|
|
1513
|
+
pattern: "^[a-f0-9]{64}$"
|
|
1514
|
+
},
|
|
1515
|
+
replayed: {
|
|
1516
|
+
type: "boolean"
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
IngestReconciliation: {
|
|
1521
|
+
type: "object",
|
|
1522
|
+
required: [
|
|
1523
|
+
"status",
|
|
1524
|
+
"body_hash"
|
|
1525
|
+
],
|
|
1526
|
+
properties: {
|
|
1527
|
+
status: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
enum: [
|
|
1530
|
+
"committed",
|
|
1531
|
+
"absent"
|
|
1532
|
+
]
|
|
1533
|
+
},
|
|
1534
|
+
body_hash: {
|
|
1535
|
+
type: "string",
|
|
1536
|
+
pattern: "^[a-f0-9]{64}$"
|
|
1537
|
+
},
|
|
1538
|
+
receipt: {
|
|
1539
|
+
$ref: "#/components/schemas/IngestReceipt"
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
description: "Read-only lookup bound to the current signed principal and exact canonical body hash. Absent does not rule out a later commit and does not authorize automatic replay."
|
|
1543
|
+
},
|
|
1544
|
+
CalendarPeriodBounds: {
|
|
1545
|
+
type: "object",
|
|
1546
|
+
properties: {
|
|
1547
|
+
time_zone: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
enum: [
|
|
1550
|
+
"UTC"
|
|
1551
|
+
]
|
|
1552
|
+
},
|
|
1553
|
+
start_date: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
format: "date",
|
|
1556
|
+
nullable: true
|
|
1557
|
+
},
|
|
1558
|
+
end_date_exclusive: {
|
|
1559
|
+
type: "string",
|
|
1560
|
+
format: "date",
|
|
1561
|
+
nullable: true
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
required: [
|
|
1565
|
+
"time_zone",
|
|
1566
|
+
"start_date",
|
|
1567
|
+
"end_date_exclusive"
|
|
1568
|
+
],
|
|
1569
|
+
description: "UTC calendar half-open dates. Null bounds mean all stored dates; weeks begin Sunday."
|
|
1570
|
+
},
|
|
1571
|
+
BillingDiffSummary: {
|
|
1572
|
+
type: "object",
|
|
1573
|
+
properties: {
|
|
1574
|
+
period: {
|
|
1575
|
+
type: "string",
|
|
1576
|
+
enum: [
|
|
1577
|
+
"today",
|
|
1578
|
+
"yesterday",
|
|
1579
|
+
"week",
|
|
1580
|
+
"month",
|
|
1581
|
+
"year",
|
|
1582
|
+
"all"
|
|
1583
|
+
]
|
|
1584
|
+
},
|
|
1585
|
+
basis: {
|
|
1586
|
+
type: "string",
|
|
1587
|
+
enum: [
|
|
1588
|
+
"unmatched_subtotals"
|
|
1589
|
+
]
|
|
1590
|
+
},
|
|
1591
|
+
estimated_usd: {
|
|
1592
|
+
type: "number",
|
|
1593
|
+
description: "Compatibility alias of usage_summary.total_usd; known stored usage-value subtotal."
|
|
1594
|
+
},
|
|
1595
|
+
actual_usd: {
|
|
1596
|
+
type: "number",
|
|
1597
|
+
description: "Compatibility alias of billing_summary.total_usd; recorded provider-row subtotal, not verified full billed cost."
|
|
1598
|
+
},
|
|
1599
|
+
usage_summary: {
|
|
1600
|
+
$ref: "#/components/schemas/CostSummary"
|
|
1601
|
+
},
|
|
1602
|
+
billing_summary: {
|
|
1603
|
+
$ref: "#/components/schemas/BillingSummary"
|
|
1604
|
+
},
|
|
1605
|
+
delta_usd: {
|
|
1606
|
+
type: "number",
|
|
1607
|
+
nullable: true,
|
|
1608
|
+
enum: [
|
|
1609
|
+
null
|
|
1610
|
+
]
|
|
1611
|
+
},
|
|
1612
|
+
delta_pct: {
|
|
1613
|
+
type: "number",
|
|
1614
|
+
nullable: true,
|
|
1615
|
+
enum: [
|
|
1616
|
+
null
|
|
1617
|
+
]
|
|
1618
|
+
},
|
|
1619
|
+
threshold_pct: {
|
|
1620
|
+
type: "number",
|
|
1621
|
+
minimum: 0
|
|
1622
|
+
},
|
|
1623
|
+
is_alert: {
|
|
1624
|
+
type: "boolean",
|
|
1625
|
+
nullable: true,
|
|
1626
|
+
enum: [
|
|
1627
|
+
null
|
|
1628
|
+
]
|
|
1629
|
+
},
|
|
1630
|
+
comparable: {
|
|
1631
|
+
type: "boolean",
|
|
1632
|
+
enum: [
|
|
1633
|
+
false
|
|
1634
|
+
]
|
|
1635
|
+
},
|
|
1636
|
+
incomparable_reason: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
enum: [
|
|
1639
|
+
"no_billing_records",
|
|
1640
|
+
"account_window_source_unverified"
|
|
1641
|
+
]
|
|
1642
|
+
},
|
|
1643
|
+
by_provider: {
|
|
1644
|
+
type: "object",
|
|
1645
|
+
additionalProperties: {
|
|
1646
|
+
type: "number"
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
by_agent: {
|
|
1650
|
+
type: "array",
|
|
1651
|
+
items: {
|
|
1652
|
+
type: "object",
|
|
1653
|
+
properties: {
|
|
1654
|
+
agent: {
|
|
1655
|
+
type: "string"
|
|
1656
|
+
},
|
|
1657
|
+
basis: {
|
|
1658
|
+
type: "string",
|
|
1659
|
+
enum: [
|
|
1660
|
+
"stored_request_rows_subtotal"
|
|
1661
|
+
]
|
|
1662
|
+
},
|
|
1663
|
+
estimated_usd: {
|
|
1664
|
+
type: "number"
|
|
1665
|
+
},
|
|
1666
|
+
actual_usd: {
|
|
1667
|
+
type: "number",
|
|
1668
|
+
nullable: true,
|
|
1669
|
+
enum: [
|
|
1670
|
+
null
|
|
1671
|
+
]
|
|
1672
|
+
},
|
|
1673
|
+
delta_usd: {
|
|
1674
|
+
type: "number",
|
|
1675
|
+
nullable: true,
|
|
1676
|
+
enum: [
|
|
1677
|
+
null
|
|
1678
|
+
]
|
|
1679
|
+
},
|
|
1680
|
+
delta_pct: {
|
|
1681
|
+
type: "number",
|
|
1682
|
+
nullable: true,
|
|
1683
|
+
enum: [
|
|
1684
|
+
null
|
|
1685
|
+
]
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
required: [
|
|
1689
|
+
"agent",
|
|
1690
|
+
"basis",
|
|
1691
|
+
"estimated_usd",
|
|
1692
|
+
"actual_usd",
|
|
1693
|
+
"delta_usd",
|
|
1694
|
+
"delta_pct"
|
|
1695
|
+
]
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
required: [
|
|
1700
|
+
"period",
|
|
1701
|
+
"basis",
|
|
1702
|
+
"estimated_usd",
|
|
1703
|
+
"actual_usd",
|
|
1704
|
+
"usage_summary",
|
|
1705
|
+
"billing_summary",
|
|
1706
|
+
"delta_usd",
|
|
1707
|
+
"delta_pct",
|
|
1708
|
+
"threshold_pct",
|
|
1709
|
+
"is_alert",
|
|
1710
|
+
"comparable",
|
|
1711
|
+
"incomparable_reason",
|
|
1712
|
+
"by_provider",
|
|
1713
|
+
"by_agent"
|
|
1714
|
+
],
|
|
1715
|
+
description: "Unmatched subtotals are not a measured difference. Account, billing-window and source correspondence are unverified; deltas and alert state remain null."
|
|
1716
|
+
},
|
|
1717
|
+
CreateApiBudgetRule: {
|
|
1718
|
+
type: "object",
|
|
1719
|
+
additionalProperties: false,
|
|
1720
|
+
required: [
|
|
1721
|
+
"id",
|
|
1722
|
+
"period",
|
|
1723
|
+
"metric",
|
|
1724
|
+
"limit_usd",
|
|
1725
|
+
"alert_at_percent",
|
|
1726
|
+
"created_at",
|
|
1727
|
+
"updated_at"
|
|
1728
|
+
],
|
|
1729
|
+
properties: {
|
|
1730
|
+
id: {
|
|
1731
|
+
type: "string",
|
|
1732
|
+
minLength: 1,
|
|
1733
|
+
maxLength: 1024
|
|
1734
|
+
},
|
|
1735
|
+
period: {
|
|
1736
|
+
type: "string",
|
|
1737
|
+
enum: [
|
|
1738
|
+
"daily",
|
|
1739
|
+
"weekly",
|
|
1740
|
+
"monthly"
|
|
1741
|
+
]
|
|
1742
|
+
},
|
|
1743
|
+
metric: {
|
|
1744
|
+
type: "string",
|
|
1745
|
+
enum: [
|
|
1746
|
+
"metered_api_usd"
|
|
1747
|
+
]
|
|
1748
|
+
},
|
|
1749
|
+
limit_usd: {
|
|
1750
|
+
type: "number",
|
|
1751
|
+
exclusiveMinimum: true,
|
|
1752
|
+
minimum: 0
|
|
1753
|
+
},
|
|
1754
|
+
alert_at_percent: {
|
|
1755
|
+
type: "integer",
|
|
1756
|
+
minimum: 1,
|
|
1757
|
+
maximum: 100
|
|
1758
|
+
},
|
|
1759
|
+
project_path: {
|
|
1760
|
+
type: "string",
|
|
1761
|
+
nullable: true,
|
|
1762
|
+
minLength: 1,
|
|
1763
|
+
maxLength: 16384
|
|
1764
|
+
},
|
|
1765
|
+
cost_center_id: {
|
|
1766
|
+
type: "string",
|
|
1767
|
+
nullable: true,
|
|
1768
|
+
minLength: 1,
|
|
1769
|
+
maxLength: 16384
|
|
1770
|
+
},
|
|
1771
|
+
agent: {
|
|
1772
|
+
type: "string",
|
|
1773
|
+
nullable: true,
|
|
1774
|
+
enum: [
|
|
1775
|
+
"claude",
|
|
1776
|
+
"takumi",
|
|
1777
|
+
"codex",
|
|
1778
|
+
"gemini",
|
|
1779
|
+
"opencode",
|
|
1780
|
+
"cursor",
|
|
1781
|
+
"pi",
|
|
1782
|
+
"hermes",
|
|
1783
|
+
null
|
|
1784
|
+
]
|
|
1785
|
+
},
|
|
1786
|
+
created_at: {
|
|
1787
|
+
type: "string",
|
|
1788
|
+
format: "date-time",
|
|
1789
|
+
description: "Explicit UTC Z timestamp; preserve exact body on retry."
|
|
1790
|
+
},
|
|
1791
|
+
updated_at: {
|
|
1792
|
+
type: "string",
|
|
1793
|
+
format: "date-time",
|
|
1794
|
+
description: "Explicit UTC Z timestamp; preserve exact body on retry."
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
CreateApiBudgetCommand: {
|
|
1799
|
+
type: "object",
|
|
1800
|
+
additionalProperties: false,
|
|
1801
|
+
required: [
|
|
1802
|
+
"schema_version",
|
|
1803
|
+
"idempotency_key",
|
|
1804
|
+
"budget_mode",
|
|
1805
|
+
"budgets"
|
|
1806
|
+
],
|
|
1807
|
+
properties: {
|
|
1808
|
+
schema_version: {
|
|
1809
|
+
type: "integer",
|
|
1810
|
+
enum: [
|
|
1811
|
+
1
|
|
1812
|
+
]
|
|
1813
|
+
},
|
|
1814
|
+
idempotency_key: {
|
|
1815
|
+
type: "string",
|
|
1816
|
+
minLength: 1,
|
|
1817
|
+
maxLength: 200,
|
|
1818
|
+
pattern: "^[A-Za-z0-9._:-]+$"
|
|
1819
|
+
},
|
|
1820
|
+
budget_mode: {
|
|
1821
|
+
type: "string",
|
|
1822
|
+
enum: [
|
|
1823
|
+
"create"
|
|
1824
|
+
]
|
|
1825
|
+
},
|
|
1826
|
+
budgets: {
|
|
1827
|
+
type: "array",
|
|
1828
|
+
minItems: 1,
|
|
1829
|
+
maxItems: 1,
|
|
1830
|
+
items: {
|
|
1831
|
+
$ref: "#/components/schemas/CreateApiBudgetRule"
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
paths: {
|
|
1839
|
+
"/health": {
|
|
1840
|
+
get: {
|
|
1841
|
+
operationId: "health",
|
|
1842
|
+
summary: "Liveness probe",
|
|
1843
|
+
security: [],
|
|
1844
|
+
responses: {
|
|
1845
|
+
"200": {
|
|
1846
|
+
description: "ok",
|
|
1847
|
+
content: {
|
|
1848
|
+
"application/json": {
|
|
1849
|
+
schema: {
|
|
1850
|
+
$ref: "#/components/schemas/Health"
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
"/ready": {
|
|
1859
|
+
get: {
|
|
1860
|
+
operationId: "ready",
|
|
1861
|
+
summary: "Readiness probe (storage reachable + migrated)",
|
|
1862
|
+
security: [],
|
|
1863
|
+
responses: {
|
|
1864
|
+
"200": {
|
|
1865
|
+
description: "ready",
|
|
1866
|
+
content: {
|
|
1867
|
+
"application/json": {
|
|
1868
|
+
schema: {
|
|
1869
|
+
$ref: "#/components/schemas/Ready"
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
},
|
|
1874
|
+
"503": {
|
|
1875
|
+
description: "not ready",
|
|
1876
|
+
content: {
|
|
1877
|
+
"application/json": {
|
|
1878
|
+
schema: {
|
|
1879
|
+
$ref: "#/components/schemas/Ready"
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
"/version": {
|
|
1888
|
+
get: {
|
|
1889
|
+
operationId: "version",
|
|
1890
|
+
summary: "Version probe",
|
|
1891
|
+
security: [],
|
|
1892
|
+
responses: {
|
|
1893
|
+
"200": {
|
|
1894
|
+
description: "version",
|
|
1895
|
+
content: {
|
|
1896
|
+
"application/json": {
|
|
1897
|
+
schema: {
|
|
1898
|
+
$ref: "#/components/schemas/Version"
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
"/v1/summary": {
|
|
1907
|
+
get: {
|
|
1908
|
+
operationId: "getSummary",
|
|
1909
|
+
summary: "Cost summary for a period",
|
|
1910
|
+
parameters: [
|
|
1911
|
+
{
|
|
1912
|
+
name: "period",
|
|
1913
|
+
in: "query",
|
|
1914
|
+
schema: {
|
|
1915
|
+
type: "string"
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
name: "machine",
|
|
1920
|
+
in: "query",
|
|
1921
|
+
schema: {
|
|
1922
|
+
type: "string"
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
],
|
|
1926
|
+
responses: {
|
|
1927
|
+
"200": {
|
|
1928
|
+
description: "ok",
|
|
1929
|
+
content: {
|
|
1930
|
+
"application/json": {
|
|
1931
|
+
schema: {
|
|
1932
|
+
type: "object",
|
|
1933
|
+
properties: {
|
|
1934
|
+
data: {
|
|
1935
|
+
$ref: "#/components/schemas/CostSummary"
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
},
|
|
1945
|
+
"/v1/sessions": {
|
|
1946
|
+
get: {
|
|
1947
|
+
operationId: "listSessions",
|
|
1948
|
+
summary: "List sessions",
|
|
1949
|
+
parameters: [
|
|
1950
|
+
{
|
|
1951
|
+
name: "agent",
|
|
1952
|
+
in: "query",
|
|
1953
|
+
schema: {
|
|
1954
|
+
type: "string"
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
name: "project",
|
|
1959
|
+
in: "query",
|
|
1960
|
+
schema: {
|
|
1961
|
+
type: "string"
|
|
1962
|
+
}
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
name: "search",
|
|
1966
|
+
in: "query",
|
|
1967
|
+
schema: {
|
|
1968
|
+
type: "string"
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
name: "machine",
|
|
1973
|
+
in: "query",
|
|
1974
|
+
schema: {
|
|
1975
|
+
type: "string"
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
name: "account",
|
|
1980
|
+
in: "query",
|
|
1981
|
+
schema: {
|
|
1982
|
+
type: "string"
|
|
1983
|
+
}
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
name: "limit",
|
|
1987
|
+
in: "query",
|
|
1988
|
+
schema: {
|
|
1989
|
+
type: "integer"
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
name: "offset",
|
|
1994
|
+
in: "query",
|
|
1995
|
+
schema: {
|
|
1996
|
+
type: "integer"
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
name: "since",
|
|
2001
|
+
in: "query",
|
|
2002
|
+
schema: {
|
|
2003
|
+
type: "string"
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
name: "fields",
|
|
2008
|
+
in: "query",
|
|
2009
|
+
schema: {
|
|
2010
|
+
type: "string"
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
],
|
|
2014
|
+
responses: {
|
|
2015
|
+
"200": {
|
|
2016
|
+
description: "ok",
|
|
2017
|
+
content: {
|
|
2018
|
+
"application/json": {
|
|
2019
|
+
schema: {
|
|
2020
|
+
type: "object",
|
|
2021
|
+
properties: {
|
|
2022
|
+
data: {
|
|
2023
|
+
type: "array",
|
|
2024
|
+
items: {
|
|
2025
|
+
$ref: "#/components/schemas/Session"
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
},
|
|
2036
|
+
"/v1/sessions/{id}/requests": {
|
|
2037
|
+
get: {
|
|
2038
|
+
operationId: "getSessionRequests",
|
|
2039
|
+
summary: "Requests for a session",
|
|
2040
|
+
parameters: [
|
|
2041
|
+
{
|
|
2042
|
+
name: "id",
|
|
2043
|
+
in: "path",
|
|
2044
|
+
required: true,
|
|
2045
|
+
schema: {
|
|
2046
|
+
type: "string"
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
],
|
|
2050
|
+
responses: {
|
|
2051
|
+
"200": {
|
|
2052
|
+
description: "ok",
|
|
2053
|
+
content: {
|
|
2054
|
+
"application/json": {
|
|
2055
|
+
schema: {
|
|
2056
|
+
$ref: "#/components/schemas/Envelope"
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
"404": {
|
|
2062
|
+
description: "not found",
|
|
2063
|
+
content: {
|
|
2064
|
+
"application/json": {
|
|
2065
|
+
schema: {
|
|
2066
|
+
$ref: "#/components/schemas/Error"
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
"/v1/top": {
|
|
2075
|
+
get: {
|
|
2076
|
+
operationId: "topSessions",
|
|
2077
|
+
summary: "Top sessions by cost",
|
|
2078
|
+
parameters: [
|
|
2079
|
+
{
|
|
2080
|
+
name: "n",
|
|
2081
|
+
in: "query",
|
|
2082
|
+
schema: {
|
|
2083
|
+
type: "integer"
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
name: "agent",
|
|
2088
|
+
in: "query",
|
|
2089
|
+
schema: {
|
|
2090
|
+
type: "string"
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: "since",
|
|
2095
|
+
in: "query",
|
|
2096
|
+
schema: {
|
|
2097
|
+
type: "string"
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
],
|
|
2101
|
+
responses: {
|
|
2102
|
+
"200": {
|
|
2103
|
+
description: "ok",
|
|
2104
|
+
content: {
|
|
2105
|
+
"application/json": {
|
|
2106
|
+
schema: {
|
|
2107
|
+
type: "object",
|
|
2108
|
+
properties: {
|
|
2109
|
+
data: {
|
|
2110
|
+
type: "array",
|
|
2111
|
+
items: {
|
|
2112
|
+
$ref: "#/components/schemas/Session"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
"/v1/models": {
|
|
2124
|
+
get: {
|
|
2125
|
+
operationId: "modelBreakdown",
|
|
2126
|
+
summary: "Cost by model",
|
|
2127
|
+
responses: {
|
|
2128
|
+
"200": {
|
|
2129
|
+
description: "ok",
|
|
2130
|
+
content: {
|
|
2131
|
+
"application/json": {
|
|
2132
|
+
schema: {
|
|
2133
|
+
type: "object",
|
|
2134
|
+
properties: {
|
|
2135
|
+
data: {
|
|
2136
|
+
type: "array",
|
|
2137
|
+
items: {
|
|
2138
|
+
$ref: "#/components/schemas/ModelBreakdown"
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
"/v1/projects": {
|
|
2150
|
+
get: {
|
|
2151
|
+
operationId: "projectBreakdown",
|
|
2152
|
+
summary: "Cost by project",
|
|
2153
|
+
parameters: [
|
|
2154
|
+
{
|
|
2155
|
+
name: "period",
|
|
2156
|
+
in: "query",
|
|
2157
|
+
schema: {
|
|
2158
|
+
type: "string"
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
name: "machine",
|
|
2163
|
+
in: "query",
|
|
2164
|
+
schema: {
|
|
2165
|
+
type: "string"
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
],
|
|
2169
|
+
responses: {
|
|
2170
|
+
"200": {
|
|
2171
|
+
description: "ok",
|
|
2172
|
+
content: {
|
|
2173
|
+
"application/json": {
|
|
2174
|
+
schema: {
|
|
2175
|
+
type: "object",
|
|
2176
|
+
properties: {
|
|
2177
|
+
data: {
|
|
2178
|
+
type: "array",
|
|
2179
|
+
items: {
|
|
2180
|
+
$ref: "#/components/schemas/ProjectBreakdown"
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
"/v1/accounts": {
|
|
2192
|
+
get: {
|
|
2193
|
+
operationId: "accountBreakdown",
|
|
2194
|
+
summary: "Cost by account",
|
|
2195
|
+
parameters: [
|
|
2196
|
+
{
|
|
2197
|
+
name: "period",
|
|
2198
|
+
in: "query",
|
|
2199
|
+
schema: {
|
|
2200
|
+
type: "string"
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
name: "machine",
|
|
2205
|
+
in: "query",
|
|
2206
|
+
schema: {
|
|
2207
|
+
type: "string"
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
],
|
|
2211
|
+
responses: {
|
|
2212
|
+
"200": {
|
|
2213
|
+
description: "ok",
|
|
2214
|
+
content: {
|
|
2215
|
+
"application/json": {
|
|
2216
|
+
schema: {
|
|
2217
|
+
type: "object",
|
|
2218
|
+
properties: {
|
|
2219
|
+
data: {
|
|
2220
|
+
type: "array",
|
|
2221
|
+
items: {
|
|
2222
|
+
$ref: "#/components/schemas/AccountBreakdown"
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
},
|
|
2233
|
+
"/v1/breakdown": {
|
|
2234
|
+
get: {
|
|
2235
|
+
operationId: "breakdown",
|
|
2236
|
+
summary: "Cost breakdown by dimension",
|
|
2237
|
+
parameters: [
|
|
2238
|
+
{
|
|
2239
|
+
name: "by",
|
|
2240
|
+
in: "query",
|
|
2241
|
+
schema: {
|
|
2242
|
+
type: "string",
|
|
2243
|
+
enum: [
|
|
2244
|
+
"model",
|
|
2245
|
+
"project",
|
|
2246
|
+
"agent",
|
|
2247
|
+
"account"
|
|
2248
|
+
]
|
|
2249
|
+
}
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
name: "period",
|
|
2253
|
+
in: "query",
|
|
2254
|
+
schema: {
|
|
2255
|
+
type: "string"
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
name: "since",
|
|
2260
|
+
in: "query",
|
|
2261
|
+
description: "ISO timestamp lower bound; takes precedence over period",
|
|
2262
|
+
schema: {
|
|
2263
|
+
type: "string",
|
|
2264
|
+
format: "date-time"
|
|
2265
|
+
}
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
name: "machine",
|
|
2269
|
+
in: "query",
|
|
2270
|
+
schema: {
|
|
2271
|
+
type: "string"
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
],
|
|
2275
|
+
responses: {
|
|
2276
|
+
"200": {
|
|
2277
|
+
description: "ok",
|
|
2278
|
+
content: {
|
|
2279
|
+
"application/json": {
|
|
2280
|
+
schema: {
|
|
2281
|
+
$ref: "#/components/schemas/Envelope"
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
},
|
|
2289
|
+
"/v1/daily": {
|
|
2290
|
+
get: {
|
|
2291
|
+
operationId: "daily",
|
|
2292
|
+
summary: "Daily cost points",
|
|
2293
|
+
parameters: [
|
|
2294
|
+
{
|
|
2295
|
+
name: "days",
|
|
2296
|
+
in: "query",
|
|
2297
|
+
schema: {
|
|
2298
|
+
type: "integer"
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "machine",
|
|
2303
|
+
in: "query",
|
|
2304
|
+
schema: {
|
|
2305
|
+
type: "string"
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
],
|
|
2309
|
+
responses: {
|
|
2310
|
+
"200": {
|
|
2311
|
+
description: "ok",
|
|
2312
|
+
content: {
|
|
2313
|
+
"application/json": {
|
|
2314
|
+
schema: {
|
|
2315
|
+
type: "object",
|
|
2316
|
+
properties: {
|
|
2317
|
+
data: {
|
|
2318
|
+
type: "array",
|
|
2319
|
+
items: {
|
|
2320
|
+
$ref: "#/components/schemas/DailyPoint"
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
"/v1/hourly": {
|
|
2332
|
+
get: {
|
|
2333
|
+
operationId: "hourly",
|
|
2334
|
+
summary: "Hourly cost points",
|
|
2335
|
+
parameters: [
|
|
2336
|
+
{
|
|
2337
|
+
name: "hours",
|
|
2338
|
+
in: "query",
|
|
2339
|
+
schema: {
|
|
2340
|
+
type: "integer"
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "machine",
|
|
2345
|
+
in: "query",
|
|
2346
|
+
schema: {
|
|
2347
|
+
type: "string"
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
],
|
|
2351
|
+
responses: {
|
|
2352
|
+
"200": {
|
|
2353
|
+
description: "ok",
|
|
2354
|
+
content: {
|
|
2355
|
+
"application/json": {
|
|
2356
|
+
schema: {
|
|
2357
|
+
$ref: "#/components/schemas/Envelope"
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2365
|
+
"/v1/machines": {
|
|
2366
|
+
get: {
|
|
2367
|
+
operationId: "listMachines",
|
|
2368
|
+
summary: "Known machines",
|
|
2369
|
+
responses: {
|
|
2370
|
+
"200": {
|
|
2371
|
+
description: "ok",
|
|
2372
|
+
content: {
|
|
2373
|
+
"application/json": {
|
|
2374
|
+
schema: {
|
|
2375
|
+
type: "object",
|
|
2376
|
+
properties: {
|
|
2377
|
+
data: {
|
|
2378
|
+
type: "array",
|
|
2379
|
+
items: {
|
|
2380
|
+
$ref: "#/components/schemas/MachineInfo"
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
"/v1/fleet": {
|
|
2392
|
+
get: {
|
|
2393
|
+
operationId: "fleet",
|
|
2394
|
+
summary: "Fleet summary",
|
|
2395
|
+
parameters: [
|
|
2396
|
+
{
|
|
2397
|
+
name: "period",
|
|
2398
|
+
in: "query",
|
|
2399
|
+
schema: {
|
|
2400
|
+
type: "string"
|
|
2401
|
+
}
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: "machine",
|
|
2405
|
+
in: "query",
|
|
2406
|
+
schema: {
|
|
2407
|
+
type: "string"
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
],
|
|
2411
|
+
responses: {
|
|
2412
|
+
"200": {
|
|
2413
|
+
description: "ok",
|
|
2414
|
+
content: {
|
|
2415
|
+
"application/json": {
|
|
2416
|
+
schema: {
|
|
2417
|
+
$ref: "#/components/schemas/Envelope"
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
},
|
|
2425
|
+
"/v1/billing": {
|
|
2426
|
+
get: {
|
|
2427
|
+
operationId: "billing",
|
|
2428
|
+
summary: "Provider billing summary",
|
|
2429
|
+
parameters: [
|
|
2430
|
+
{
|
|
2431
|
+
name: "period",
|
|
2432
|
+
in: "query",
|
|
2433
|
+
schema: {
|
|
2434
|
+
type: "string"
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
],
|
|
2438
|
+
responses: {
|
|
2439
|
+
"200": {
|
|
2440
|
+
description: "ok",
|
|
2441
|
+
content: {
|
|
2442
|
+
"application/json": {
|
|
2443
|
+
schema: {
|
|
2444
|
+
type: "object",
|
|
2445
|
+
properties: {
|
|
2446
|
+
data: {
|
|
2447
|
+
$ref: "#/components/schemas/BillingSummary"
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
},
|
|
2457
|
+
"/v1/billing/diff": {
|
|
2458
|
+
get: {
|
|
2459
|
+
operationId: "billingDiff",
|
|
2460
|
+
summary: "Read unmatched usage-value and provider-row subtotals with an explicit unavailable comparison",
|
|
2461
|
+
parameters: [
|
|
2462
|
+
{
|
|
2463
|
+
name: "period",
|
|
2464
|
+
in: "query",
|
|
2465
|
+
schema: {
|
|
2466
|
+
type: "string"
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
name: "threshold",
|
|
2471
|
+
in: "query",
|
|
2472
|
+
schema: {
|
|
2473
|
+
type: "number"
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
],
|
|
2477
|
+
responses: {
|
|
2478
|
+
"200": {
|
|
2479
|
+
description: "ok",
|
|
2480
|
+
content: {
|
|
2481
|
+
"application/json": {
|
|
2482
|
+
schema: {
|
|
2483
|
+
type: "object",
|
|
2484
|
+
properties: {
|
|
2485
|
+
data: {
|
|
2486
|
+
$ref: "#/components/schemas/BillingDiffSummary"
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
required: [
|
|
2490
|
+
"data"
|
|
2491
|
+
]
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
"/v1/usage": {
|
|
2500
|
+
get: {
|
|
2501
|
+
operationId: "usage",
|
|
2502
|
+
summary: "Usage snapshots + summary",
|
|
2503
|
+
parameters: [
|
|
2504
|
+
{
|
|
2505
|
+
name: "period",
|
|
2506
|
+
in: "query",
|
|
2507
|
+
schema: {
|
|
2508
|
+
type: "string"
|
|
2509
|
+
}
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
name: "agent",
|
|
2513
|
+
in: "query",
|
|
2514
|
+
schema: {
|
|
2515
|
+
type: "string"
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
],
|
|
2519
|
+
responses: {
|
|
2520
|
+
"200": {
|
|
2521
|
+
description: "ok",
|
|
2522
|
+
content: {
|
|
2523
|
+
"application/json": {
|
|
2524
|
+
schema: {
|
|
2525
|
+
$ref: "#/components/schemas/Envelope"
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
},
|
|
2533
|
+
"/v1/savings": {
|
|
2534
|
+
get: {
|
|
2535
|
+
operationId: "savings",
|
|
2536
|
+
summary: "Savings summary",
|
|
2537
|
+
parameters: [
|
|
2538
|
+
{
|
|
2539
|
+
name: "period",
|
|
2540
|
+
in: "query",
|
|
2541
|
+
schema: {
|
|
2542
|
+
type: "string"
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
{
|
|
2546
|
+
name: "agent",
|
|
2547
|
+
in: "query",
|
|
2548
|
+
schema: {
|
|
2549
|
+
type: "string"
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
],
|
|
2553
|
+
responses: {
|
|
2554
|
+
"200": {
|
|
2555
|
+
description: "ok",
|
|
2556
|
+
content: {
|
|
2557
|
+
"application/json": {
|
|
2558
|
+
schema: {
|
|
2559
|
+
$ref: "#/components/schemas/Envelope"
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
"/v1/budgets": {
|
|
2568
|
+
get: {
|
|
2569
|
+
operationId: "listBudgets",
|
|
2570
|
+
summary: "List budgets",
|
|
2571
|
+
responses: {
|
|
2572
|
+
"200": {
|
|
2573
|
+
description: "ok",
|
|
2574
|
+
content: {
|
|
2575
|
+
"application/json": {
|
|
2576
|
+
schema: {
|
|
2577
|
+
type: "object",
|
|
2578
|
+
properties: {
|
|
2579
|
+
data: {
|
|
2580
|
+
type: "array",
|
|
2581
|
+
items: {
|
|
2582
|
+
$ref: "#/components/schemas/BudgetStatus"
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
},
|
|
2592
|
+
post: {
|
|
2593
|
+
operationId: "createBudget",
|
|
2594
|
+
summary: "Create a budget",
|
|
2595
|
+
requestBody: {
|
|
2596
|
+
required: true,
|
|
2597
|
+
content: {
|
|
2598
|
+
"application/json": {
|
|
2599
|
+
schema: {
|
|
2600
|
+
$ref: "#/components/schemas/CreateBudgetInput"
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
},
|
|
2605
|
+
responses: {
|
|
2606
|
+
"200": {
|
|
2607
|
+
description: "created",
|
|
2608
|
+
content: {
|
|
2609
|
+
"application/json": {
|
|
2610
|
+
schema: {
|
|
2611
|
+
type: "object",
|
|
2612
|
+
properties: {
|
|
2613
|
+
data: {
|
|
2614
|
+
$ref: "#/components/schemas/BudgetStatus"
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
},
|
|
2624
|
+
"/v1/budgets/{id}": {
|
|
2625
|
+
delete: {
|
|
2626
|
+
operationId: "deleteBudget",
|
|
2627
|
+
summary: "Delete a budget",
|
|
2628
|
+
parameters: [
|
|
2629
|
+
{
|
|
2630
|
+
name: "id",
|
|
2631
|
+
in: "path",
|
|
2632
|
+
required: true,
|
|
2633
|
+
schema: {
|
|
2634
|
+
type: "string"
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
],
|
|
2638
|
+
responses: {
|
|
2639
|
+
"200": {
|
|
2640
|
+
description: "ok",
|
|
2641
|
+
content: {
|
|
2642
|
+
"application/json": {
|
|
2643
|
+
schema: {
|
|
2644
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
"/v1/goals": {
|
|
2653
|
+
get: {
|
|
2654
|
+
operationId: "listGoals",
|
|
2655
|
+
summary: "List goals",
|
|
2656
|
+
responses: {
|
|
2657
|
+
"200": {
|
|
2658
|
+
description: "ok",
|
|
2659
|
+
content: {
|
|
2660
|
+
"application/json": {
|
|
2661
|
+
schema: {
|
|
2662
|
+
type: "object",
|
|
2663
|
+
properties: {
|
|
2664
|
+
data: {
|
|
2665
|
+
type: "array",
|
|
2666
|
+
items: {
|
|
2667
|
+
$ref: "#/components/schemas/GoalStatus"
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
},
|
|
2677
|
+
post: {
|
|
2678
|
+
operationId: "createGoal",
|
|
2679
|
+
summary: "Create a goal",
|
|
2680
|
+
requestBody: {
|
|
2681
|
+
required: true,
|
|
2682
|
+
content: {
|
|
2683
|
+
"application/json": {
|
|
2684
|
+
schema: {
|
|
2685
|
+
$ref: "#/components/schemas/CreateGoalInput"
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2690
|
+
responses: {
|
|
2691
|
+
"200": {
|
|
2692
|
+
description: "created",
|
|
2693
|
+
content: {
|
|
2694
|
+
"application/json": {
|
|
2695
|
+
schema: {
|
|
2696
|
+
type: "object",
|
|
2697
|
+
properties: {
|
|
2698
|
+
data: {
|
|
2699
|
+
$ref: "#/components/schemas/GoalStatus"
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
},
|
|
2709
|
+
"/v1/goals/{id}": {
|
|
2710
|
+
delete: {
|
|
2711
|
+
operationId: "deleteGoal",
|
|
2712
|
+
summary: "Delete a goal",
|
|
2713
|
+
parameters: [
|
|
2714
|
+
{
|
|
2715
|
+
name: "id",
|
|
2716
|
+
in: "path",
|
|
2717
|
+
required: true,
|
|
2718
|
+
schema: {
|
|
2719
|
+
type: "string"
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
],
|
|
2723
|
+
responses: {
|
|
2724
|
+
"200": {
|
|
2725
|
+
description: "ok",
|
|
2726
|
+
content: {
|
|
2727
|
+
"application/json": {
|
|
2728
|
+
schema: {
|
|
2729
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
"/v1/pricing": {
|
|
2738
|
+
get: {
|
|
2739
|
+
operationId: "listPricing",
|
|
2740
|
+
summary: "List model pricing",
|
|
2741
|
+
responses: {
|
|
2742
|
+
"200": {
|
|
2743
|
+
description: "ok",
|
|
2744
|
+
content: {
|
|
2745
|
+
"application/json": {
|
|
2746
|
+
schema: {
|
|
2747
|
+
type: "object",
|
|
2748
|
+
properties: {
|
|
2749
|
+
data: {
|
|
2750
|
+
type: "array",
|
|
2751
|
+
items: {
|
|
2752
|
+
$ref: "#/components/schemas/ModelPricing"
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
},
|
|
2762
|
+
post: {
|
|
2763
|
+
operationId: "upsertPricing",
|
|
2764
|
+
summary: "Upsert model pricing",
|
|
2765
|
+
requestBody: {
|
|
2766
|
+
required: true,
|
|
2767
|
+
content: {
|
|
2768
|
+
"application/json": {
|
|
2769
|
+
schema: {
|
|
2770
|
+
$ref: "#/components/schemas/CreatePricingInput"
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
},
|
|
2775
|
+
responses: {
|
|
2776
|
+
"200": {
|
|
2777
|
+
description: "ok",
|
|
2778
|
+
content: {
|
|
2779
|
+
"application/json": {
|
|
2780
|
+
schema: {
|
|
2781
|
+
type: "object",
|
|
2782
|
+
properties: {
|
|
2783
|
+
data: {
|
|
2784
|
+
$ref: "#/components/schemas/ModelPricing"
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
},
|
|
2794
|
+
"/v1/pricing/{model}": {
|
|
2795
|
+
delete: {
|
|
2796
|
+
operationId: "deletePricing",
|
|
2797
|
+
summary: "Delete model pricing",
|
|
2798
|
+
parameters: [
|
|
2799
|
+
{
|
|
2800
|
+
name: "model",
|
|
2801
|
+
in: "path",
|
|
2802
|
+
required: true,
|
|
2803
|
+
schema: {
|
|
2804
|
+
type: "string"
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
],
|
|
2808
|
+
responses: {
|
|
2809
|
+
"200": {
|
|
2810
|
+
description: "ok",
|
|
2811
|
+
content: {
|
|
2812
|
+
"application/json": {
|
|
2813
|
+
schema: {
|
|
2814
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2822
|
+
"/v1/subscriptions": {
|
|
2823
|
+
get: {
|
|
2824
|
+
operationId: "listSubscriptions",
|
|
2825
|
+
summary: "List subscriptions",
|
|
2826
|
+
responses: {
|
|
2827
|
+
"200": {
|
|
2828
|
+
description: "ok",
|
|
2829
|
+
content: {
|
|
2830
|
+
"application/json": {
|
|
2831
|
+
schema: {
|
|
2832
|
+
type: "object",
|
|
2833
|
+
properties: {
|
|
2834
|
+
data: {
|
|
2835
|
+
type: "array",
|
|
2836
|
+
items: {
|
|
2837
|
+
$ref: "#/components/schemas/Subscription"
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
post: {
|
|
2848
|
+
operationId: "upsertSubscription",
|
|
2849
|
+
summary: "Upsert a subscription",
|
|
2850
|
+
requestBody: {
|
|
2851
|
+
required: true,
|
|
2852
|
+
content: {
|
|
2853
|
+
"application/json": {
|
|
2854
|
+
schema: {
|
|
2855
|
+
$ref: "#/components/schemas/CreateSubscriptionInput"
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
},
|
|
2860
|
+
responses: {
|
|
2861
|
+
"200": {
|
|
2862
|
+
description: "ok",
|
|
2863
|
+
content: {
|
|
2864
|
+
"application/json": {
|
|
2865
|
+
schema: {
|
|
2866
|
+
type: "object",
|
|
2867
|
+
properties: {
|
|
2868
|
+
data: {
|
|
2869
|
+
$ref: "#/components/schemas/Subscription"
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2879
|
+
"/v1/subscriptions/{id}": {
|
|
2880
|
+
delete: {
|
|
2881
|
+
operationId: "deleteSubscription",
|
|
2882
|
+
summary: "Delete a subscription",
|
|
2883
|
+
parameters: [
|
|
2884
|
+
{
|
|
2885
|
+
name: "id",
|
|
2886
|
+
in: "path",
|
|
2887
|
+
required: true,
|
|
2888
|
+
schema: {
|
|
2889
|
+
type: "string"
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
],
|
|
2893
|
+
responses: {
|
|
2894
|
+
"200": {
|
|
2895
|
+
description: "ok",
|
|
2896
|
+
content: {
|
|
2897
|
+
"application/json": {
|
|
2898
|
+
schema: {
|
|
2899
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
},
|
|
2907
|
+
"/v1/project-registry": {
|
|
2908
|
+
get: {
|
|
2909
|
+
operationId: "listProjectRegistry",
|
|
2910
|
+
summary: "List registered projects",
|
|
2911
|
+
responses: {
|
|
2912
|
+
"200": {
|
|
2913
|
+
description: "ok",
|
|
2914
|
+
content: {
|
|
2915
|
+
"application/json": {
|
|
2916
|
+
schema: {
|
|
2917
|
+
type: "object",
|
|
2918
|
+
properties: {
|
|
2919
|
+
data: {
|
|
2920
|
+
type: "array",
|
|
2921
|
+
items: {
|
|
2922
|
+
$ref: "#/components/schemas/Project"
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
},
|
|
2932
|
+
post: {
|
|
2933
|
+
operationId: "upsertProject",
|
|
2934
|
+
summary: "Register a project",
|
|
2935
|
+
requestBody: {
|
|
2936
|
+
required: true,
|
|
2937
|
+
content: {
|
|
2938
|
+
"application/json": {
|
|
2939
|
+
schema: {
|
|
2940
|
+
$ref: "#/components/schemas/CreateProjectInput"
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
},
|
|
2945
|
+
responses: {
|
|
2946
|
+
"200": {
|
|
2947
|
+
description: "ok",
|
|
2948
|
+
content: {
|
|
2949
|
+
"application/json": {
|
|
2950
|
+
schema: {
|
|
2951
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
},
|
|
2959
|
+
"/v1/project-registry/{id}": {
|
|
2960
|
+
delete: {
|
|
2961
|
+
operationId: "deleteProject",
|
|
2962
|
+
summary: "Delete a registered project",
|
|
2963
|
+
parameters: [
|
|
2964
|
+
{
|
|
2965
|
+
name: "id",
|
|
2966
|
+
in: "path",
|
|
2967
|
+
required: true,
|
|
2968
|
+
schema: {
|
|
2969
|
+
type: "string"
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
],
|
|
2973
|
+
responses: {
|
|
2974
|
+
"200": {
|
|
2975
|
+
description: "ok",
|
|
2976
|
+
content: {
|
|
2977
|
+
"application/json": {
|
|
2978
|
+
schema: {
|
|
2979
|
+
$ref: "#/components/schemas/MutationOk"
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
},
|
|
2987
|
+
"/v1/sync": {
|
|
2988
|
+
post: {
|
|
2989
|
+
operationId: "sync",
|
|
2990
|
+
summary: "Trigger an ingest sync",
|
|
2991
|
+
requestBody: {
|
|
2992
|
+
required: false,
|
|
2993
|
+
content: {
|
|
2994
|
+
"application/json": {
|
|
2995
|
+
schema: {
|
|
2996
|
+
type: "object",
|
|
2997
|
+
properties: {
|
|
2998
|
+
sources: {
|
|
2999
|
+
type: "string"
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
},
|
|
3006
|
+
responses: {
|
|
3007
|
+
"200": {
|
|
3008
|
+
description: "ok",
|
|
3009
|
+
content: {
|
|
3010
|
+
"application/json": {
|
|
3011
|
+
schema: {
|
|
3012
|
+
$ref: "#/components/schemas/Envelope"
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
"/v1/ingest": {
|
|
3021
|
+
post: {
|
|
3022
|
+
operationId: "ingest",
|
|
3023
|
+
summary: "Atomically ingest a bounded, versioned observation batch",
|
|
3024
|
+
requestBody: {
|
|
3025
|
+
required: true,
|
|
3026
|
+
content: {
|
|
3027
|
+
"application/json": {
|
|
3028
|
+
schema: {
|
|
3029
|
+
type: "object",
|
|
3030
|
+
properties: {
|
|
3031
|
+
requests: {
|
|
3032
|
+
type: "array",
|
|
3033
|
+
items: {
|
|
3034
|
+
type: "object"
|
|
3035
|
+
},
|
|
3036
|
+
maxItems: 1e4
|
|
3037
|
+
},
|
|
3038
|
+
sessions: {
|
|
3039
|
+
type: "array",
|
|
3040
|
+
items: {
|
|
3041
|
+
type: "object"
|
|
3042
|
+
},
|
|
3043
|
+
maxItems: 1e4
|
|
3044
|
+
},
|
|
3045
|
+
projects: {
|
|
3046
|
+
type: "array",
|
|
3047
|
+
items: {
|
|
3048
|
+
type: "object"
|
|
3049
|
+
},
|
|
3050
|
+
maxItems: 1e4
|
|
3051
|
+
},
|
|
3052
|
+
budgets: {
|
|
3053
|
+
type: "array",
|
|
3054
|
+
items: {
|
|
3055
|
+
type: "object"
|
|
3056
|
+
},
|
|
3057
|
+
maxItems: 1e4
|
|
3058
|
+
},
|
|
3059
|
+
goals: {
|
|
3060
|
+
type: "array",
|
|
3061
|
+
items: {
|
|
3062
|
+
type: "object"
|
|
3063
|
+
},
|
|
3064
|
+
maxItems: 1e4
|
|
3065
|
+
},
|
|
3066
|
+
billing_daily: {
|
|
3067
|
+
type: "array",
|
|
3068
|
+
items: {
|
|
3069
|
+
type: "object"
|
|
3070
|
+
},
|
|
3071
|
+
maxItems: 1e4
|
|
3072
|
+
},
|
|
3073
|
+
model_pricing: {
|
|
3074
|
+
type: "array",
|
|
3075
|
+
items: {
|
|
3076
|
+
type: "object"
|
|
3077
|
+
},
|
|
3078
|
+
maxItems: 1e4
|
|
3079
|
+
},
|
|
3080
|
+
subscriptions: {
|
|
3081
|
+
type: "array",
|
|
3082
|
+
items: {
|
|
3083
|
+
$ref: "#/components/schemas/Subscription"
|
|
3084
|
+
},
|
|
3085
|
+
maxItems: 1e4
|
|
3086
|
+
},
|
|
3087
|
+
usage_snapshots: {
|
|
3088
|
+
type: "array",
|
|
3089
|
+
items: {
|
|
3090
|
+
$ref: "#/components/schemas/UsageSnapshot"
|
|
3091
|
+
},
|
|
3092
|
+
maxItems: 1e4
|
|
3093
|
+
},
|
|
3094
|
+
schema_version: {
|
|
3095
|
+
type: "integer",
|
|
3096
|
+
enum: [
|
|
3097
|
+
1
|
|
3098
|
+
],
|
|
3099
|
+
description: "Optional format version; current value is 1."
|
|
3100
|
+
},
|
|
3101
|
+
idempotency_key: {
|
|
3102
|
+
type: "string",
|
|
3103
|
+
minLength: 1,
|
|
3104
|
+
maxLength: 200,
|
|
3105
|
+
pattern: "^[A-Za-z0-9._:-]+$",
|
|
3106
|
+
description: "Stable caller-owned key for the exact body. After uncertainty use GET /v1/ingest/receipts with the canonical body hash; do not automatically replay."
|
|
3107
|
+
},
|
|
3108
|
+
scan_receipts: {
|
|
3109
|
+
type: "array",
|
|
3110
|
+
items: {
|
|
3111
|
+
$ref: "#/components/schemas/ScanReceipt"
|
|
3112
|
+
},
|
|
3113
|
+
maxItems: 1e4
|
|
3114
|
+
},
|
|
3115
|
+
subscription_mode: {
|
|
3116
|
+
type: "string",
|
|
3117
|
+
enum: [
|
|
3118
|
+
"quota_observation",
|
|
3119
|
+
"configuration"
|
|
3120
|
+
],
|
|
3121
|
+
default: "quota_observation",
|
|
3122
|
+
description: "With subscriptions only. Quota observations preserve existing assignment, activation, plan label and cycle settings. Explicit configuration permits changing those fields under the current write authority."
|
|
3123
|
+
},
|
|
3124
|
+
budget_mode: {
|
|
3125
|
+
type: "string",
|
|
3126
|
+
enum: [
|
|
3127
|
+
"create"
|
|
3128
|
+
],
|
|
3129
|
+
description: "Opt-in new metered API rule creation. Requires schema_version=1, stable idempotency_key, exactly one budgets row matching CreateApiBudgetRule and no other tables. Existing IDs conflict; preserve complete command/body on uncertainty and use the existing receipt GET. Does not dispatch notifications."
|
|
3130
|
+
}
|
|
3131
|
+
},
|
|
3132
|
+
additionalProperties: false
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
},
|
|
3137
|
+
responses: {
|
|
3138
|
+
"200": {
|
|
3139
|
+
description: "Committed or exact replayed receipt.",
|
|
3140
|
+
content: {
|
|
3141
|
+
"application/json": {
|
|
3142
|
+
schema: {
|
|
3143
|
+
type: "object",
|
|
3144
|
+
required: [
|
|
3145
|
+
"data"
|
|
3146
|
+
],
|
|
3147
|
+
properties: {
|
|
3148
|
+
data: {
|
|
3149
|
+
$ref: "#/components/schemas/IngestReceipt"
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
},
|
|
3156
|
+
"400": {
|
|
3157
|
+
description: "INVALID_INGEST: invalid field or row; no writes.",
|
|
3158
|
+
content: {
|
|
3159
|
+
"application/json": {
|
|
3160
|
+
schema: {
|
|
3161
|
+
type: "object",
|
|
3162
|
+
required: [
|
|
3163
|
+
"error"
|
|
3164
|
+
],
|
|
3165
|
+
properties: {
|
|
3166
|
+
error: {
|
|
3167
|
+
type: "string"
|
|
3168
|
+
},
|
|
3169
|
+
code: {
|
|
3170
|
+
type: "string"
|
|
3171
|
+
},
|
|
3172
|
+
field: {
|
|
3173
|
+
type: "string"
|
|
3174
|
+
},
|
|
3175
|
+
expected: {
|
|
3176
|
+
type: "string"
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
},
|
|
3183
|
+
"409": {
|
|
3184
|
+
description: "INGEST_CONFLICT: changed key content, concurrent row change, or missing/conflicting source version; no writes for this command.",
|
|
3185
|
+
content: {
|
|
3186
|
+
"application/json": {
|
|
3187
|
+
schema: {
|
|
3188
|
+
type: "object",
|
|
3189
|
+
required: [
|
|
3190
|
+
"error"
|
|
3191
|
+
],
|
|
3192
|
+
properties: {
|
|
3193
|
+
error: {
|
|
3194
|
+
type: "string"
|
|
3195
|
+
},
|
|
3196
|
+
code: {
|
|
3197
|
+
type: "string"
|
|
3198
|
+
},
|
|
3199
|
+
field: {
|
|
3200
|
+
type: "string"
|
|
3201
|
+
},
|
|
3202
|
+
expected: {
|
|
3203
|
+
type: "string"
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
},
|
|
3210
|
+
"413": {
|
|
3211
|
+
description: "INGEST_TOO_LARGE: split into bounded commands; no writes.",
|
|
3212
|
+
content: {
|
|
3213
|
+
"application/json": {
|
|
3214
|
+
schema: {
|
|
3215
|
+
type: "object",
|
|
3216
|
+
required: [
|
|
3217
|
+
"error"
|
|
3218
|
+
],
|
|
3219
|
+
properties: {
|
|
3220
|
+
error: {
|
|
3221
|
+
type: "string"
|
|
3222
|
+
},
|
|
3223
|
+
code: {
|
|
3224
|
+
type: "string"
|
|
3225
|
+
},
|
|
3226
|
+
field: {
|
|
3227
|
+
type: "string"
|
|
3228
|
+
},
|
|
3229
|
+
expected: {
|
|
3230
|
+
type: "string"
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
},
|
|
3238
|
+
description: "Maximum 10000 rows total and 4194304 JSON bytes. All rows and rollups commit with one actor-bound receipt. A repeated exact key/content returns that receipt with replayed=true; different content under the same key is 409. Older source versions are skipped; conflicting same-version or unversioned replacements are 409. Missing key derives from exact content for compatibility. Source times use ISO UTC (Z). No automatic retry after an uncertain response. Legacy Codex aggregate and event rows for one session cannot coexist; migrate preserved historical data separately."
|
|
3239
|
+
}
|
|
3240
|
+
},
|
|
3241
|
+
"/v1/ingest/receipts": {
|
|
3242
|
+
get: {
|
|
3243
|
+
operationId: "reconcileIngestReceipt",
|
|
3244
|
+
summary: "Read the exact current-principal ingestion receipt without replay",
|
|
3245
|
+
parameters: [
|
|
3246
|
+
{
|
|
3247
|
+
name: "idempotency_key",
|
|
3248
|
+
in: "query",
|
|
3249
|
+
required: true,
|
|
3250
|
+
schema: {
|
|
3251
|
+
type: "string",
|
|
3252
|
+
pattern: "^[A-Za-z0-9._:-]{1,200}$"
|
|
3253
|
+
}
|
|
3254
|
+
},
|
|
3255
|
+
{
|
|
3256
|
+
name: "body_hash",
|
|
3257
|
+
in: "query",
|
|
3258
|
+
required: true,
|
|
3259
|
+
schema: {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
pattern: "^[a-f0-9]{64}$"
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
],
|
|
3265
|
+
responses: {
|
|
3266
|
+
"200": {
|
|
3267
|
+
description: "ok",
|
|
3268
|
+
content: {
|
|
3269
|
+
"application/json": {
|
|
3270
|
+
schema: {
|
|
3271
|
+
type: "object",
|
|
3272
|
+
required: [
|
|
3273
|
+
"data"
|
|
3274
|
+
],
|
|
3275
|
+
properties: {
|
|
3276
|
+
data: {
|
|
3277
|
+
$ref: "#/components/schemas/IngestReconciliation"
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
},
|
|
3284
|
+
"400": {
|
|
3285
|
+
description: "Invalid query shape; exactly one key and hash are required."
|
|
3286
|
+
},
|
|
3287
|
+
"401": {
|
|
3288
|
+
description: "Current principal is missing or invalid."
|
|
3289
|
+
},
|
|
3290
|
+
"409": {
|
|
3291
|
+
description: "The key exists for this principal with different content."
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
"/v1/scan-receipts": {
|
|
3297
|
+
get: {
|
|
3298
|
+
operationId: "listScanReceipts",
|
|
3299
|
+
summary: "Bounded latest collection attempts; not a complete fleet coverage claim",
|
|
3300
|
+
parameters: [
|
|
3301
|
+
{
|
|
3302
|
+
name: "machine_id",
|
|
3303
|
+
in: "query",
|
|
3304
|
+
schema: {
|
|
3305
|
+
type: "string"
|
|
3306
|
+
}
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
name: "source",
|
|
3310
|
+
in: "query",
|
|
3311
|
+
schema: {
|
|
3312
|
+
type: "string"
|
|
3313
|
+
}
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
name: "limit",
|
|
3317
|
+
in: "query",
|
|
3318
|
+
schema: {
|
|
3319
|
+
type: "integer",
|
|
3320
|
+
minimum: 1,
|
|
3321
|
+
maximum: 200,
|
|
3322
|
+
default: 100
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
],
|
|
3326
|
+
responses: {
|
|
3327
|
+
"200": {
|
|
3328
|
+
description: "ok",
|
|
3329
|
+
content: {
|
|
3330
|
+
"application/json": {
|
|
3331
|
+
schema: {
|
|
3332
|
+
type: "object",
|
|
3333
|
+
required: [
|
|
3334
|
+
"data"
|
|
3335
|
+
],
|
|
3336
|
+
properties: {
|
|
3337
|
+
data: {
|
|
3338
|
+
type: "array",
|
|
3339
|
+
items: {
|
|
3340
|
+
$ref: "#/components/schemas/ScanReceipt"
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
},
|
|
3348
|
+
"400": {
|
|
3349
|
+
description: "Invalid limit; expected an integer from 1 to 200."
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
},
|
|
3354
|
+
"/v1/auth/session": {
|
|
3355
|
+
get: {
|
|
3356
|
+
operationId: "humanSession",
|
|
3357
|
+
summary: "Read the authenticated human and effective store-bound scopes",
|
|
3358
|
+
security: [
|
|
3359
|
+
{
|
|
3360
|
+
bearer: []
|
|
3361
|
+
}
|
|
3362
|
+
],
|
|
3363
|
+
responses: {
|
|
3364
|
+
"200": {
|
|
3365
|
+
description: "Current human session; Cache-Control: no-store",
|
|
3366
|
+
content: {
|
|
3367
|
+
"application/json": {
|
|
3368
|
+
schema: {
|
|
3369
|
+
type: "object",
|
|
3370
|
+
required: [
|
|
3371
|
+
"data",
|
|
3372
|
+
"meta"
|
|
3373
|
+
],
|
|
3374
|
+
properties: {
|
|
3375
|
+
data: {
|
|
3376
|
+
type: "object",
|
|
3377
|
+
required: [
|
|
3378
|
+
"subject",
|
|
3379
|
+
"tenant_id",
|
|
3380
|
+
"scopes",
|
|
3381
|
+
"session_id",
|
|
3382
|
+
"expires_at"
|
|
3383
|
+
],
|
|
3384
|
+
properties: {
|
|
3385
|
+
subject: {
|
|
3386
|
+
type: "string"
|
|
3387
|
+
},
|
|
3388
|
+
tenant_id: {
|
|
3389
|
+
type: "string"
|
|
3390
|
+
},
|
|
3391
|
+
scopes: {
|
|
3392
|
+
type: "array",
|
|
3393
|
+
items: {
|
|
3394
|
+
type: "string",
|
|
3395
|
+
enum: [
|
|
3396
|
+
"economy:read",
|
|
3397
|
+
"economy:write"
|
|
3398
|
+
]
|
|
3399
|
+
}
|
|
3400
|
+
},
|
|
3401
|
+
session_id: {
|
|
3402
|
+
type: "string"
|
|
3403
|
+
},
|
|
3404
|
+
expires_at: {
|
|
3405
|
+
type: "integer",
|
|
3406
|
+
description: "Expiry in epoch seconds"
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
},
|
|
3410
|
+
meta: {
|
|
3411
|
+
type: "object"
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
},
|
|
3418
|
+
"401": {
|
|
3419
|
+
description: "Invalid or expired human token"
|
|
3420
|
+
},
|
|
3421
|
+
"403": {
|
|
3422
|
+
description: "Human, tenant, principal or current grant denied"
|
|
3423
|
+
},
|
|
3424
|
+
"503": {
|
|
3425
|
+
description: "Human binding missing or identity authority unavailable"
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
// src/server/service-dispatcher.ts
|
|
3434
|
+
import { fork } from "child_process";
|
|
3435
|
+
import { randomUUID } from "crypto";
|
|
3436
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3437
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3438
|
+
|
|
3439
|
+
// src/server/service-protocol.ts
|
|
3440
|
+
var routes = [
|
|
3441
|
+
["summary", "GET", "/api/summary"],
|
|
3442
|
+
["machines", "GET", "/api/machines"],
|
|
3443
|
+
["fleet", "GET", "/api/fleet"],
|
|
3444
|
+
["daily", "GET", "/api/daily"],
|
|
3445
|
+
["hourly", "GET", "/api/hourly"],
|
|
3446
|
+
["sessions", "GET", "/api/sessions"],
|
|
3447
|
+
["top", "GET", "/api/top"],
|
|
3448
|
+
["models", "GET", "/api/models"],
|
|
3449
|
+
["billing", "GET", "/api/billing"],
|
|
3450
|
+
["billingDiff", "GET", "/api/billing/diff"],
|
|
3451
|
+
["billingSync", "POST", "/api/billing/sync"],
|
|
3452
|
+
["projects", "GET", "/api/projects"],
|
|
3453
|
+
["accounts", "GET", "/api/accounts"],
|
|
3454
|
+
["breakdown", "GET", "/api/breakdown"],
|
|
3455
|
+
["budgets", "GET", "/api/budgets"],
|
|
3456
|
+
["budgetSave", "POST", "/api/budgets"],
|
|
3457
|
+
["projectRegistry", "GET", "/api/project-registry"],
|
|
3458
|
+
["projectSave", "POST", "/api/project-registry"],
|
|
3459
|
+
["pricing", "GET", "/api/pricing"],
|
|
3460
|
+
["pricingSave", "POST", "/api/pricing"],
|
|
3461
|
+
["sync", "POST", "/api/sync"],
|
|
3462
|
+
["ingest", "POST", "/api/ingest"],
|
|
3463
|
+
["ingestReceipt", "GET", "/api/ingest/receipts"],
|
|
3464
|
+
["scanReceipts", "GET", "/api/scan-receipts"],
|
|
3465
|
+
["usage", "GET", "/api/usage"],
|
|
3466
|
+
["savings", "GET", "/api/savings"],
|
|
3467
|
+
["brief", "GET", "/api/brief"],
|
|
3468
|
+
["projectDetail", "GET", "/api/projects/detail"],
|
|
3469
|
+
["export", "GET", "/api/export"],
|
|
3470
|
+
["compare", "GET", "/api/compare"],
|
|
3471
|
+
["forecast", "GET", "/api/forecast"],
|
|
3472
|
+
["efficiency", "GET", "/api/efficiency"],
|
|
3473
|
+
["requests", "GET", "/api/requests"],
|
|
3474
|
+
["subscriptions", "GET", "/api/subscriptions"],
|
|
3475
|
+
["subscriptionSave", "POST", "/api/subscriptions"],
|
|
3476
|
+
["goals", "GET", "/api/goals"],
|
|
3477
|
+
["goalSave", "POST", "/api/goals"],
|
|
3478
|
+
["feedback", "POST", "/api/feedback"]
|
|
3479
|
+
];
|
|
3480
|
+
var dynamicRoutes = [
|
|
3481
|
+
["budgetDelete", "DELETE", /^\/api\/budgets\/(.+)$/],
|
|
3482
|
+
["projectDelete", "DELETE", /^\/api\/project-registry\/(.+)$/],
|
|
3483
|
+
["pricingDelete", "DELETE", /^\/api\/pricing\/(.+)$/],
|
|
3484
|
+
["subscriptionDelete", "DELETE", /^\/api\/subscriptions\/(.+)$/],
|
|
3485
|
+
["goalDelete", "DELETE", /^\/api\/goals\/(.+)$/],
|
|
3486
|
+
["sessionRequests", "GET", /^\/api\/sessions\/([^/]+)\/requests$/]
|
|
3487
|
+
];
|
|
3488
|
+
function serviceRoute(method, path) {
|
|
3489
|
+
return routes.find((route) => route[1] === method && route[2] === path)?.[0] ?? dynamicRoutes.find((route) => route[1] === method && route[2].test(path))?.[0];
|
|
3490
|
+
}
|
|
3491
|
+
function validOperation(operation) {
|
|
3492
|
+
return !!operation && typeof operation.path === "string" && typeof operation.method === "string" && typeof operation.route === "string" && serviceRoute(operation.method, operation.path) === operation.route && operation.path.length <= 8192 && operation.search?.length <= 8192 && typeof operation.search === "string" && (operation.search === "" || operation.search.startsWith("?")) && typeof operation.actorId === "string" && operation.actorId.length <= 1024 && (operation.body === null || typeof operation.body === "string");
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// src/server/service-dispatcher.ts
|
|
3496
|
+
var DEFAULT_SERVICE_LIMITS = {
|
|
3497
|
+
workers: 2,
|
|
3498
|
+
maxQueue: 32,
|
|
3499
|
+
deadlineMs: 30000,
|
|
3500
|
+
maxInputBytes: 4 * 1024 * 1024,
|
|
3501
|
+
maxResultBytes: 8 * 1024 * 1024,
|
|
3502
|
+
maxQueuedBytes: 16 * 1024 * 1024
|
|
3503
|
+
};
|
|
3504
|
+
function serviceLimits(env = process.env) {
|
|
3505
|
+
const limits = { ...DEFAULT_SERVICE_LIMITS };
|
|
3506
|
+
for (const [field, name, max] of [
|
|
3507
|
+
["workers", "ECONOMY_SERVICE_WORKERS", 8],
|
|
3508
|
+
["maxQueue", "ECONOMY_SERVICE_MAX_QUEUE", 256],
|
|
3509
|
+
["deadlineMs", "ECONOMY_SERVICE_DEADLINE_MS", 120000],
|
|
3510
|
+
["maxInputBytes", "ECONOMY_SERVICE_MAX_INPUT_BYTES", 16 * 1024 * 1024],
|
|
3511
|
+
["maxResultBytes", "ECONOMY_SERVICE_MAX_RESULT_BYTES", 32 * 1024 * 1024],
|
|
3512
|
+
["maxQueuedBytes", "ECONOMY_SERVICE_MAX_QUEUED_BYTES", 64 * 1024 * 1024]
|
|
3513
|
+
]) {
|
|
3514
|
+
if (env[name] === undefined)
|
|
3515
|
+
continue;
|
|
3516
|
+
const value = Number(env[name]);
|
|
3517
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
3518
|
+
throw new Error(`Invalid ${name}`);
|
|
3519
|
+
limits[field] = value;
|
|
3520
|
+
}
|
|
3521
|
+
return limits;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
class ServiceUnavailable extends Error {
|
|
3525
|
+
code;
|
|
3526
|
+
status;
|
|
3527
|
+
operationId;
|
|
3528
|
+
outcome;
|
|
3529
|
+
constructor(code, status, operationId, outcome) {
|
|
3530
|
+
super("Economy operation could not return a confirmed result");
|
|
3531
|
+
this.code = code;
|
|
3532
|
+
this.status = status;
|
|
3533
|
+
this.operationId = operationId;
|
|
3534
|
+
this.outcome = outcome;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
class AsyncServiceDispatcher {
|
|
3539
|
+
backend;
|
|
3540
|
+
limits;
|
|
3541
|
+
queue = [];
|
|
3542
|
+
active = new Set;
|
|
3543
|
+
generation = 0;
|
|
3544
|
+
queuedBytes = 0;
|
|
3545
|
+
stopped = false;
|
|
3546
|
+
constructor(backend, limits = serviceLimits()) {
|
|
3547
|
+
this.backend = backend;
|
|
3548
|
+
this.limits = limits;
|
|
3549
|
+
}
|
|
3550
|
+
dispatch(operation, signal, beforeStart) {
|
|
3551
|
+
const id = randomUUID();
|
|
3552
|
+
if (!validOperation(operation))
|
|
3553
|
+
return Promise.reject(new ServiceUnavailable("invalid_operation", 400, id, "not_started"));
|
|
3554
|
+
const bodyBytes = operation.body === null ? 0 : Buffer.byteLength(operation.body);
|
|
3555
|
+
const bytes = bodyBytes + Buffer.byteLength(JSON.stringify({ ...operation, body: null }));
|
|
3556
|
+
if (bodyBytes > this.limits.maxInputBytes)
|
|
3557
|
+
return Promise.reject(new ServiceUnavailable("input_too_large", 413, id, "not_started"));
|
|
3558
|
+
if (this.stopped)
|
|
3559
|
+
return Promise.reject(new ServiceUnavailable("service_stopping", 503, id, "not_started"));
|
|
3560
|
+
if (signal?.aborted)
|
|
3561
|
+
return Promise.reject(new ServiceUnavailable("request_cancelled", 499, id, "not_started"));
|
|
3562
|
+
if (this.active.size >= this.limits.workers && (this.queue.length >= this.limits.maxQueue || this.queuedBytes + bytes > this.limits.maxQueuedBytes)) {
|
|
3563
|
+
return Promise.reject(new ServiceUnavailable("service_busy", 503, id, "not_started"));
|
|
3564
|
+
}
|
|
3565
|
+
return new Promise((resolve, reject) => {
|
|
3566
|
+
const job = {
|
|
3567
|
+
id,
|
|
3568
|
+
generation: ++this.generation,
|
|
3569
|
+
operation,
|
|
3570
|
+
bytes,
|
|
3571
|
+
deadline: performance.now() + this.limits.deadlineMs,
|
|
3572
|
+
done: false,
|
|
3573
|
+
resolve,
|
|
3574
|
+
reject,
|
|
3575
|
+
signal,
|
|
3576
|
+
beforeStart,
|
|
3577
|
+
cancel: () => {},
|
|
3578
|
+
timer: undefined
|
|
3579
|
+
};
|
|
3580
|
+
job.cancel = () => this.fail(job, "request_cancelled", 499);
|
|
3581
|
+
signal?.addEventListener("abort", job.cancel, { once: true });
|
|
3582
|
+
job.timer = setTimeout(() => this.fail(job, "operation_timeout", 504), this.limits.deadlineMs);
|
|
3583
|
+
this.queue.push(job);
|
|
3584
|
+
this.queuedBytes += bytes;
|
|
3585
|
+
this.pump();
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
settle(job) {
|
|
3589
|
+
if (job.done)
|
|
3590
|
+
return false;
|
|
3591
|
+
job.done = true;
|
|
3592
|
+
clearTimeout(job.timer);
|
|
3593
|
+
job.signal?.removeEventListener("abort", job.cancel);
|
|
3594
|
+
const index = this.queue.indexOf(job);
|
|
3595
|
+
if (index !== -1) {
|
|
3596
|
+
this.queue.splice(index, 1);
|
|
3597
|
+
this.queuedBytes -= job.bytes;
|
|
3598
|
+
}
|
|
3599
|
+
return true;
|
|
3600
|
+
}
|
|
3601
|
+
fail(job, code, status) {
|
|
3602
|
+
if (!this.settle(job))
|
|
3603
|
+
return;
|
|
3604
|
+
job.reject(new ServiceUnavailable(code, status, job.id, job.child ? "unknown" : "not_started"));
|
|
3605
|
+
if (job.child && job.child.exitCode === null && job.child.signalCode === null)
|
|
3606
|
+
job.child.kill("SIGKILL");
|
|
3607
|
+
this.pump();
|
|
3608
|
+
}
|
|
3609
|
+
pump() {
|
|
3610
|
+
while (!this.stopped && this.active.size < this.limits.workers && this.queue.length) {
|
|
3611
|
+
const job = this.queue.shift();
|
|
3612
|
+
this.queuedBytes -= job.bytes;
|
|
3613
|
+
if (job.done)
|
|
3614
|
+
continue;
|
|
3615
|
+
if (performance.now() >= job.deadline) {
|
|
3616
|
+
this.fail(job, "operation_timeout", 504);
|
|
3617
|
+
continue;
|
|
3618
|
+
}
|
|
3619
|
+
this.start(job);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
start(job) {
|
|
3623
|
+
this.active.add(job);
|
|
3624
|
+
if (!job.beforeStart) {
|
|
3625
|
+
this.launch(job);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
(async () => {
|
|
3629
|
+
try {
|
|
3630
|
+
await job.beforeStart();
|
|
3631
|
+
if (!job.done && !this.stopped && performance.now() < job.deadline) {
|
|
3632
|
+
this.launch(job);
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
this.fail(job, "operation_timeout", 504);
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
this.fail(job, error instanceof ServiceUnavailable ? error.code : "authentication_unavailable", error instanceof ServiceUnavailable ? error.status : 503);
|
|
3638
|
+
}
|
|
3639
|
+
this.active.delete(job);
|
|
3640
|
+
this.pump();
|
|
3641
|
+
})();
|
|
3642
|
+
}
|
|
3643
|
+
launch(job) {
|
|
3644
|
+
const self = fileURLToPath2(import.meta.url);
|
|
3645
|
+
const workerPath = join2(dirname2(self), `service-worker.${self.endsWith(".js") ? "js" : "ts"}`);
|
|
3646
|
+
const env = { ...process.env, ECONOMY_PG_POOL_MAX: "1" };
|
|
3647
|
+
for (const key of ["HASNA_ECONOMY_API_SIGNING_KEY", "HASNA_API_SIGNING_KEY", "API_KEY_SIGNING_SECRET", "NODE_OPTIONS", "BUN_OPTIONS"])
|
|
3648
|
+
delete env[key];
|
|
3649
|
+
try {
|
|
3650
|
+
const child = fork(workerPath, [], { execPath: process.execPath, env, stdio: ["ignore", "ignore", "ignore", "ipc"], serialization: "json" });
|
|
3651
|
+
job.child = child;
|
|
3652
|
+
child.on("message", (input) => {
|
|
3653
|
+
if (job.done || !this.active.has(job))
|
|
3654
|
+
return;
|
|
3655
|
+
const reply = input;
|
|
3656
|
+
if (reply?.version !== 1 || reply.id !== job.id || reply.generation !== job.generation) {
|
|
3657
|
+
this.fail(job, "worker_protocol_error", 502);
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (performance.now() >= job.deadline) {
|
|
3661
|
+
this.fail(job, "operation_timeout", 504);
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
if (!reply.ok) {
|
|
3665
|
+
this.fail(job, reply.code === "result_too_large" ? "result_too_large" : "operation_failed", 502);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (!reply.result || !Number.isInteger(reply.result.status) || reply.result.status < 100 || reply.result.status > 599 || typeof reply.result.body !== "string" || Buffer.byteLength(reply.result.body) > this.limits.maxResultBytes) {
|
|
3669
|
+
this.fail(job, "worker_protocol_error", 502);
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
if (this.settle(job)) {
|
|
3673
|
+
job.resolve(reply.result);
|
|
3674
|
+
child.kill("SIGKILL");
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
child.on("error", () => {
|
|
3678
|
+
this.fail(job, "worker_unavailable", 503);
|
|
3679
|
+
if (!child.pid) {
|
|
3680
|
+
this.active.delete(job);
|
|
3681
|
+
this.pump();
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3684
|
+
child.once("exit", () => {
|
|
3685
|
+
if (!job.done)
|
|
3686
|
+
this.fail(job, "worker_exited", 502);
|
|
3687
|
+
this.active.delete(job);
|
|
3688
|
+
this.pump();
|
|
3689
|
+
});
|
|
3690
|
+
child.send({
|
|
3691
|
+
version: 1,
|
|
3692
|
+
id: job.id,
|
|
3693
|
+
generation: job.generation,
|
|
3694
|
+
operation: job.operation,
|
|
3695
|
+
backend: this.backend,
|
|
3696
|
+
queryTimeoutMs: Math.max(1, Math.floor(job.deadline - performance.now())),
|
|
3697
|
+
maxResultBytes: this.limits.maxResultBytes
|
|
3698
|
+
}, (error) => {
|
|
3699
|
+
if (error)
|
|
3700
|
+
this.fail(job, "worker_unavailable", 503);
|
|
3701
|
+
});
|
|
3702
|
+
} catch {
|
|
3703
|
+
this.fail(job, "worker_unavailable", 503);
|
|
3704
|
+
this.active.delete(job);
|
|
3705
|
+
this.pump();
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
state() {
|
|
3709
|
+
return { active: this.active.size, queued: this.queue.length, queuedBytes: this.queuedBytes, stopped: this.stopped };
|
|
3710
|
+
}
|
|
3711
|
+
async close() {
|
|
3712
|
+
this.stopped = true;
|
|
3713
|
+
for (const job of [...this.queue, ...this.active])
|
|
3714
|
+
this.fail(job, "service_stopping", 503);
|
|
3715
|
+
await Promise.all([...this.active].map((job) => new Promise((resolve, reject) => {
|
|
3716
|
+
if (!job.child || job.child.exitCode !== null || job.child.signalCode !== null) {
|
|
3717
|
+
resolve();
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
const timer = setTimeout(() => reject(new Error("Economy worker shutdown timed out")), 2000);
|
|
3721
|
+
job.child.once("exit", () => {
|
|
3722
|
+
clearTimeout(timer);
|
|
3723
|
+
resolve();
|
|
3724
|
+
});
|
|
3725
|
+
})));
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// src/server/service-operations.ts
|
|
3730
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3731
|
+
var CORS = {
|
|
3732
|
+
"Access-Control-Allow-Origin": "*",
|
|
3733
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
3734
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
3735
|
+
};
|
|
3736
|
+
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
3737
|
+
var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
3738
|
+
function json(data, status = 200) {
|
|
3739
|
+
return new Response(JSON.stringify(data), {
|
|
3740
|
+
status,
|
|
3741
|
+
headers: { "Content-Type": "application/json", ...CORS }
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
function ok(data, meta) {
|
|
3745
|
+
return json({ data, meta: meta ?? {} });
|
|
3746
|
+
}
|
|
3747
|
+
function err(message, status = 400) {
|
|
3748
|
+
return json({ error: message }, status);
|
|
3749
|
+
}
|
|
3750
|
+
function normalizeBudgetPeriod(value) {
|
|
3751
|
+
switch (value) {
|
|
3752
|
+
case "day":
|
|
3753
|
+
case "daily":
|
|
3754
|
+
return "daily";
|
|
3755
|
+
case "week":
|
|
3756
|
+
case "weekly":
|
|
3757
|
+
return "weekly";
|
|
3758
|
+
case "month":
|
|
3759
|
+
case "monthly":
|
|
3760
|
+
default:
|
|
3761
|
+
return "monthly";
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
function finiteNumber(value) {
|
|
3765
|
+
const n = Number(value);
|
|
3766
|
+
return Number.isFinite(n) ? n : null;
|
|
3767
|
+
}
|
|
3768
|
+
async function jsonBody(req) {
|
|
3769
|
+
const body = await req.json().catch(() => null);
|
|
3770
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
3771
|
+
}
|
|
3772
|
+
function optionalString(value) {
|
|
3773
|
+
return typeof value === "string" ? value : null;
|
|
3774
|
+
}
|
|
3775
|
+
function optionalAgent(value) {
|
|
3776
|
+
if (value == null || value === "")
|
|
3777
|
+
return null;
|
|
3778
|
+
return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
|
|
3779
|
+
}
|
|
3780
|
+
function stringArray(value) {
|
|
3781
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
3782
|
+
}
|
|
3783
|
+
function applyFields(obj, fields) {
|
|
3784
|
+
if (!fields || fields.length === 0)
|
|
3785
|
+
return obj;
|
|
3786
|
+
return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
|
|
3787
|
+
}
|
|
3788
|
+
function createDataHandler(db, actorId) {
|
|
3789
|
+
return async function handler(req) {
|
|
3790
|
+
const url = new URL(req.url);
|
|
3791
|
+
const path = url.pathname;
|
|
3792
|
+
const method = req.method;
|
|
3793
|
+
if (path === "/api/summary" && method === "GET") {
|
|
3794
|
+
const period = url.searchParams.get("period") ?? "today";
|
|
3795
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3796
|
+
return ok(querySummary(db, period, machine));
|
|
3797
|
+
}
|
|
3798
|
+
if (path === "/api/machines" && method === "GET") {
|
|
3799
|
+
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
3800
|
+
}
|
|
3801
|
+
if (path === "/api/fleet" && method === "GET") {
|
|
3802
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3803
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3804
|
+
return ok({
|
|
3805
|
+
summary: querySummary(db, period, machine),
|
|
3806
|
+
machines: listMachines(db, period),
|
|
3807
|
+
registry: listMachineRegistry(db),
|
|
3808
|
+
current_machine: getMachineId()
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
if (path === "/api/daily" && method === "GET") {
|
|
3812
|
+
const days = Number(url.searchParams.get("days") ?? 30);
|
|
3813
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3814
|
+
return ok(queryDailyBreakdown(db, days, machine));
|
|
3815
|
+
}
|
|
3816
|
+
if (path === "/api/hourly" && method === "GET") {
|
|
3817
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3818
|
+
const rawHours = url.searchParams.get("hours");
|
|
3819
|
+
let hours;
|
|
3820
|
+
if (rawHours != null) {
|
|
3821
|
+
const parsedHours = Number(rawHours);
|
|
3822
|
+
if (!Number.isInteger(parsedHours) || parsedHours < 1 || parsedHours > 48) {
|
|
3823
|
+
return err("hours must be between 1 and 48");
|
|
3824
|
+
}
|
|
3825
|
+
hours = parsedHours;
|
|
3826
|
+
}
|
|
3827
|
+
return ok(queryHourlyBreakdown(db, machine, hours));
|
|
3828
|
+
}
|
|
3829
|
+
if (path === "/api/sessions" && method === "GET") {
|
|
3830
|
+
const agent = url.searchParams.get("agent");
|
|
3831
|
+
const project = url.searchParams.get("project") ?? undefined;
|
|
3832
|
+
const search = url.searchParams.get("search") ?? undefined;
|
|
3833
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3834
|
+
const account = url.searchParams.get("account") ?? undefined;
|
|
3835
|
+
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3836
|
+
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
3837
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
3838
|
+
const fieldsParam = url.searchParams.get("fields");
|
|
3839
|
+
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
3840
|
+
const sessions = querySessions(db, {
|
|
3841
|
+
agent: agent ?? undefined,
|
|
3842
|
+
project,
|
|
3843
|
+
search,
|
|
3844
|
+
machine,
|
|
3845
|
+
account,
|
|
3846
|
+
limit,
|
|
3847
|
+
offset,
|
|
3848
|
+
since
|
|
3849
|
+
});
|
|
3850
|
+
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
3851
|
+
}
|
|
3852
|
+
if (path === "/api/top" && method === "GET") {
|
|
3853
|
+
const n = Number(url.searchParams.get("n") ?? 10);
|
|
3854
|
+
const agent = url.searchParams.get("agent") ?? undefined;
|
|
3855
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
3856
|
+
return ok(queryTopSessions(db, n, agent, since));
|
|
3857
|
+
}
|
|
3858
|
+
if (path === "/api/models" && method === "GET") {
|
|
3859
|
+
return ok(queryModelBreakdown(db));
|
|
3860
|
+
}
|
|
3861
|
+
if (path === "/api/billing" && method === "GET") {
|
|
3862
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3863
|
+
return ok(queryBillingSummary(db, period));
|
|
3864
|
+
}
|
|
3865
|
+
if (path === "/api/billing/diff" && method === "GET") {
|
|
3866
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
3867
|
+
const threshold = Number(url.searchParams.get("threshold") ?? 15);
|
|
3868
|
+
return ok(queryBillingDiff(db, period, Number.isFinite(threshold) ? threshold : 15));
|
|
3869
|
+
}
|
|
3870
|
+
if (path === "/api/billing/sync" && method === "POST") {
|
|
3871
|
+
const body = await jsonBody(req) ?? {};
|
|
3872
|
+
const days = Number(body["days"] ?? 31);
|
|
3873
|
+
if (!Number.isFinite(days) || days <= 0 || days > 366)
|
|
3874
|
+
return err("days must be between 1 and 366");
|
|
3875
|
+
const providers = Array.isArray(body["providers"]) ? body["providers"] : ["anthropic", "openai", "gemini"];
|
|
3876
|
+
const allowedProviders = new Set(["anthropic", "openai", "gemini"]);
|
|
3877
|
+
if (providers.some((provider) => !allowedProviders.has(provider)))
|
|
3878
|
+
return err("invalid billing provider");
|
|
3879
|
+
const results = {};
|
|
3880
|
+
const { syncAnthropicBilling, syncOpenAIBilling, syncGeminiBilling } = await import("./billing-bkpxy4kv.js");
|
|
3881
|
+
async function capture(provider, fn) {
|
|
3882
|
+
try {
|
|
3883
|
+
results[provider] = await fn();
|
|
3884
|
+
} catch (error) {
|
|
3885
|
+
const missingConfig = [
|
|
3886
|
+
"Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)",
|
|
3887
|
+
"Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)"
|
|
3888
|
+
];
|
|
3889
|
+
const safeMessage = error instanceof Error && missingConfig.includes(error.message) ? error.message : "billing_sync_failed";
|
|
3890
|
+
results[provider] = { error: safeMessage };
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
if (providers.includes("anthropic"))
|
|
3894
|
+
await capture("anthropic", () => syncAnthropicBilling(db, { days }));
|
|
3895
|
+
if (providers.includes("openai"))
|
|
3896
|
+
await capture("openai", () => syncOpenAIBilling(db, { days }));
|
|
3897
|
+
if (providers.includes("gemini"))
|
|
3898
|
+
await capture("gemini", () => syncGeminiBilling(db, { days }));
|
|
3899
|
+
return ok(results);
|
|
3900
|
+
}
|
|
3901
|
+
if (path === "/api/projects" && method === "GET") {
|
|
3902
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3903
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3904
|
+
return ok(queryProjectBreakdown(db, period, machine));
|
|
3905
|
+
}
|
|
3906
|
+
if (path === "/api/accounts" && method === "GET") {
|
|
3907
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3908
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3909
|
+
return ok(queryAccountBreakdown(db, period, machine));
|
|
3910
|
+
}
|
|
3911
|
+
if (path === "/api/breakdown" && method === "GET") {
|
|
3912
|
+
const by = url.searchParams.get("by") ?? "model";
|
|
3913
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3914
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
3915
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3916
|
+
if (by === "project")
|
|
3917
|
+
return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
|
|
3918
|
+
if (by === "agent")
|
|
3919
|
+
return ok(since ? queryAgentBreakdownSince(db, since) : queryAgentBreakdown(db, period, machine));
|
|
3920
|
+
if (by === "account")
|
|
3921
|
+
return ok(since ? queryAccountBreakdownSince(db, since) : queryAccountBreakdown(db, period, machine));
|
|
3922
|
+
if (by === "cost-center")
|
|
3923
|
+
return ok(queryCostCenterBreakdown(db, period, { machine, since }));
|
|
3924
|
+
if (["loop", "app", "repo", "service", "team"].includes(by))
|
|
3925
|
+
return ok(queryCostCenterBreakdown(db, period, { kind: by, machine, since }));
|
|
3926
|
+
return ok(since ? queryModelBreakdownSince(db, since) : queryModelBreakdown(db));
|
|
3927
|
+
}
|
|
3928
|
+
if (path === "/api/budgets" && method === "GET") {
|
|
3929
|
+
return ok(getBudgetStatuses(db));
|
|
3930
|
+
}
|
|
3931
|
+
if (path === "/api/budgets" && method === "POST") {
|
|
3932
|
+
const body = await jsonBody(req);
|
|
3933
|
+
if (!body)
|
|
3934
|
+
return err("invalid JSON body");
|
|
3935
|
+
const limitUsd = finiteNumber(body["limit_usd"]);
|
|
3936
|
+
const alertAtPercent = finiteNumber(body["alert_at_percent"] ?? 80);
|
|
3937
|
+
if (limitUsd == null || limitUsd <= 0)
|
|
3938
|
+
return err("limit_usd must be a positive number");
|
|
3939
|
+
if (alertAtPercent == null || alertAtPercent <= 0 || alertAtPercent > 100)
|
|
3940
|
+
return err("alert_at_percent must be between 1 and 100");
|
|
3941
|
+
const agent = optionalAgent(body["agent"]);
|
|
3942
|
+
if (agent === undefined)
|
|
3943
|
+
return err(AGENT_ERROR);
|
|
3944
|
+
const now = new Date().toISOString();
|
|
3945
|
+
const budget = {
|
|
3946
|
+
id: randomUUID2(),
|
|
3947
|
+
project_path: body["project_path"] ?? null,
|
|
3948
|
+
agent,
|
|
3949
|
+
cost_center_id: optionalString(body["cost_center_id"]) ?? null,
|
|
3950
|
+
period: normalizeBudgetPeriod(body["period"]),
|
|
3951
|
+
limit_usd: limitUsd,
|
|
3952
|
+
alert_at_percent: alertAtPercent,
|
|
3953
|
+
created_at: now,
|
|
3954
|
+
updated_at: now
|
|
3955
|
+
};
|
|
3956
|
+
upsertBudget(db, budget);
|
|
3957
|
+
return ok(getBudgetStatuses(db).find((b) => b.id === budget.id) ?? budget);
|
|
3958
|
+
}
|
|
3959
|
+
const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
|
|
3960
|
+
if (budgetMatch && method === "DELETE") {
|
|
3961
|
+
deleteBudget(db, decodeURIComponent(budgetMatch[1]));
|
|
3962
|
+
return ok({ ok: true });
|
|
3963
|
+
}
|
|
3964
|
+
if (path === "/api/project-registry" && method === "GET") {
|
|
3965
|
+
return ok(listProjects(db));
|
|
3966
|
+
}
|
|
3967
|
+
if (path === "/api/project-registry" && method === "POST") {
|
|
3968
|
+
const body = await jsonBody(req);
|
|
3969
|
+
if (!body)
|
|
3970
|
+
return err("invalid JSON body");
|
|
3971
|
+
const { basename } = await import("path");
|
|
3972
|
+
const projPath = optionalString(body["path"])?.trim();
|
|
3973
|
+
if (!projPath)
|
|
3974
|
+
return err("path is required");
|
|
3975
|
+
upsertProject(db, {
|
|
3976
|
+
id: randomUUID2(),
|
|
3977
|
+
path: projPath,
|
|
3978
|
+
name: optionalString(body["name"]) ?? basename(projPath),
|
|
3979
|
+
description: optionalString(body["description"]),
|
|
3980
|
+
tags: stringArray(body["tags"]),
|
|
3981
|
+
created_at: new Date().toISOString()
|
|
3982
|
+
});
|
|
3983
|
+
return ok({ ok: true });
|
|
3984
|
+
}
|
|
3985
|
+
const projMatch = path.match(/^\/api\/project-registry\/(.+)$/);
|
|
3986
|
+
if (projMatch && method === "DELETE") {
|
|
3987
|
+
deleteProject(db, decodeURIComponent(projMatch[1]));
|
|
3988
|
+
return ok({ ok: true });
|
|
3989
|
+
}
|
|
3990
|
+
if (path === "/api/pricing" && method === "GET") {
|
|
3991
|
+
return ok(listModelPricing(db));
|
|
3992
|
+
}
|
|
3993
|
+
if (path === "/api/pricing" && method === "POST") {
|
|
3994
|
+
const body = await jsonBody(req);
|
|
3995
|
+
if (!body)
|
|
3996
|
+
return err("invalid JSON body");
|
|
3997
|
+
const model = String(body["model"] ?? "").trim();
|
|
3998
|
+
if (!model)
|
|
3999
|
+
return err("model is required");
|
|
4000
|
+
const input = finiteNumber(body["input_per_1m"]);
|
|
4001
|
+
const output = finiteNumber(body["output_per_1m"]);
|
|
4002
|
+
const cacheRead = finiteNumber(body["cache_read_per_1m"] ?? 0);
|
|
4003
|
+
const cacheWrite = finiteNumber(body["cache_write_per_1m"] ?? 0);
|
|
4004
|
+
const cacheWrite1h = finiteNumber(body["cache_write_1h_per_1m"] ?? 0);
|
|
4005
|
+
const cacheStorage = finiteNumber(body["cache_storage_per_1m_hour"] ?? 0);
|
|
4006
|
+
if ([input, output, cacheRead, cacheWrite, cacheWrite1h, cacheStorage].some((v) => v == null || v < 0)) {
|
|
4007
|
+
return err("pricing values must be non-negative numbers");
|
|
4008
|
+
}
|
|
4009
|
+
const pricing = {
|
|
4010
|
+
model,
|
|
4011
|
+
input_per_1m: input,
|
|
4012
|
+
output_per_1m: output,
|
|
4013
|
+
cache_read_per_1m: cacheRead,
|
|
4014
|
+
cache_write_per_1m: cacheWrite,
|
|
4015
|
+
cache_write_1h_per_1m: cacheWrite1h,
|
|
4016
|
+
cache_storage_per_1m_hour: cacheStorage,
|
|
4017
|
+
updated_at: new Date().toISOString()
|
|
4018
|
+
};
|
|
4019
|
+
upsertModelPricing(db, pricing);
|
|
4020
|
+
return ok(pricing);
|
|
4021
|
+
}
|
|
4022
|
+
const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
|
|
4023
|
+
if (pricingMatch && method === "DELETE") {
|
|
4024
|
+
deleteModelPricing(db, decodeURIComponent(pricingMatch[1]));
|
|
4025
|
+
return ok({ ok: true });
|
|
4026
|
+
}
|
|
4027
|
+
if (path === "/api/sync" && method === "POST") {
|
|
4028
|
+
const body = await jsonBody(req) ?? {};
|
|
4029
|
+
const sources = body["sources"] ?? "all";
|
|
4030
|
+
if (!SYNC_SOURCES.includes(sources))
|
|
4031
|
+
return err("invalid sync source");
|
|
4032
|
+
const results = {};
|
|
4033
|
+
if (sources === "all") {
|
|
4034
|
+
try {
|
|
4035
|
+
const { syncOpenProjectsRegistry } = await import("./open-projects-6z4pq6mx.js");
|
|
4036
|
+
results["projects"] = await syncOpenProjectsRegistry(db);
|
|
4037
|
+
} catch {}
|
|
4038
|
+
}
|
|
4039
|
+
const selected = sources === "all" ? {} : { [sources]: true };
|
|
4040
|
+
const syncResult = await syncAll(db, selected);
|
|
4041
|
+
Object.assign(results, syncResult);
|
|
4042
|
+
try {
|
|
4043
|
+
const { checkAndFireWebhooks } = await import("./webhooks-k4fh2t9c.js");
|
|
4044
|
+
await checkAndFireWebhooks(db);
|
|
4045
|
+
} catch {}
|
|
4046
|
+
return ok(results);
|
|
4047
|
+
}
|
|
4048
|
+
if (path === "/api/ingest/receipts" && method === "GET") {
|
|
4049
|
+
const params = url.searchParams;
|
|
4050
|
+
if (params.getAll("idempotency_key").length !== 1 || params.getAll("body_hash").length !== 1 || [...params.keys()].some((k) => !["idempotency_key", "body_hash"].includes(k))) {
|
|
4051
|
+
throw new IngestError(400, "INVALID_INGEST", "query", "expected exactly one idempotency_key and one body_hash");
|
|
4052
|
+
}
|
|
4053
|
+
return ok(reconcileIngestReceipt(db, params.get("idempotency_key"), params.get("body_hash"), { actorId }));
|
|
4054
|
+
}
|
|
4055
|
+
if (path === "/api/scan-receipts" && method === "GET") {
|
|
4056
|
+
const limit = Number(url.searchParams.get("limit") ?? 100);
|
|
4057
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
4058
|
+
return err("limit must be an integer from 1 to 200");
|
|
4059
|
+
return ok(queryScanReceipts(db, { machine_id: url.searchParams.get("machine_id") ?? undefined, source: url.searchParams.get("source") ?? undefined, limit }));
|
|
4060
|
+
}
|
|
4061
|
+
if (path === "/api/ingest" && method === "POST") {
|
|
4062
|
+
const body = await jsonBody(req);
|
|
4063
|
+
if (!body)
|
|
4064
|
+
return err("invalid JSON body");
|
|
4065
|
+
const result = bulkIngest(db, body, { actorId });
|
|
4066
|
+
try {
|
|
4067
|
+
const { checkAndFireWebhooks } = await import("./webhooks-k4fh2t9c.js");
|
|
4068
|
+
if (body["budget_mode"] !== "create")
|
|
4069
|
+
await checkAndFireWebhooks(db);
|
|
4070
|
+
} catch {}
|
|
4071
|
+
return ok(result);
|
|
4072
|
+
}
|
|
4073
|
+
if (path === "/api/usage" && method === "GET") {
|
|
4074
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
4075
|
+
const agentParam = url.searchParams.get("agent") ?? undefined;
|
|
4076
|
+
const agent = agentParam && isAgent(agentParam) ? agentParam : undefined;
|
|
4077
|
+
return ok({
|
|
4078
|
+
snapshots: queryUsageSnapshots(db, {
|
|
4079
|
+
agent,
|
|
4080
|
+
...usageSnapshotFilterForPeriod(period)
|
|
4081
|
+
}),
|
|
4082
|
+
summary: querySummary(db, period, undefined, true, agent)
|
|
4083
|
+
});
|
|
4084
|
+
}
|
|
4085
|
+
if (path === "/api/savings" && method === "GET") {
|
|
4086
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
4087
|
+
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4088
|
+
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
4089
|
+
}
|
|
4090
|
+
if (path === "/api/brief" && method === "GET") {
|
|
4091
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
4092
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4093
|
+
try {
|
|
4094
|
+
return ok(buildBrief(db, { since, machine }));
|
|
4095
|
+
} catch (error) {
|
|
4096
|
+
const invalidSince = "--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date";
|
|
4097
|
+
if (error instanceof Error && error.message === invalidSince)
|
|
4098
|
+
return err(invalidSince);
|
|
4099
|
+
return err("brief_unavailable", 503);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
if (path === "/api/projects/detail" && method === "GET") {
|
|
4103
|
+
const q = url.searchParams.get("q")?.trim();
|
|
4104
|
+
if (!q)
|
|
4105
|
+
return err("q is required");
|
|
4106
|
+
return ok(queryProjectDetail(db, q));
|
|
4107
|
+
}
|
|
4108
|
+
if (path === "/api/export" && method === "GET") {
|
|
4109
|
+
const type = url.searchParams.get("type") ?? "sessions";
|
|
4110
|
+
if (type !== "sessions" && type !== "requests")
|
|
4111
|
+
return err("type must be sessions or requests");
|
|
4112
|
+
const period = url.searchParams.get("period") ?? "month";
|
|
4113
|
+
return ok(queryExportRows(db, type, period));
|
|
4114
|
+
}
|
|
4115
|
+
if (path === "/api/compare" && method === "GET") {
|
|
4116
|
+
const from = url.searchParams.get("from")?.trim();
|
|
4117
|
+
const to = url.searchParams.get("to")?.trim();
|
|
4118
|
+
if (!from || !to)
|
|
4119
|
+
return err("from and to are required (YYYY-MM-DD)");
|
|
4120
|
+
return ok(queryRangeStats(db, from, to));
|
|
4121
|
+
}
|
|
4122
|
+
if (path === "/api/forecast" && method === "GET") {
|
|
4123
|
+
return ok(queryForecast(db));
|
|
4124
|
+
}
|
|
4125
|
+
if (path === "/api/efficiency" && method === "GET") {
|
|
4126
|
+
return ok(queryModelEfficiency(db));
|
|
4127
|
+
}
|
|
4128
|
+
if (path === "/api/requests" && method === "GET") {
|
|
4129
|
+
const since = url.searchParams.get("since")?.trim();
|
|
4130
|
+
if (!since)
|
|
4131
|
+
return err("since is required (ISO timestamp)");
|
|
4132
|
+
return ok(queryRequestsSince(db, since));
|
|
4133
|
+
}
|
|
4134
|
+
if (path === "/api/subscriptions" && method === "GET") {
|
|
4135
|
+
return ok(listSubscriptions(db));
|
|
4136
|
+
}
|
|
4137
|
+
if (path === "/api/subscriptions" && method === "POST") {
|
|
4138
|
+
const body = await jsonBody(req);
|
|
4139
|
+
if (!body)
|
|
4140
|
+
return err("invalid JSON body");
|
|
4141
|
+
const provider = optionalString(body["provider"])?.trim();
|
|
4142
|
+
const plan = optionalString(body["plan"])?.trim();
|
|
4143
|
+
if (!provider)
|
|
4144
|
+
return err("provider is required");
|
|
4145
|
+
if (!plan)
|
|
4146
|
+
return err("plan is required");
|
|
4147
|
+
for (const field of ["account_key", "observed_station", "observed_at"]) {
|
|
4148
|
+
const value = body[field];
|
|
4149
|
+
if (value !== undefined && !(field !== "account_key" && value === null) && (typeof value !== "string" || !value.trim() || value.length > 256))
|
|
4150
|
+
return err(`${field} must be nonempty text of at most 256 characters${field !== "account_key" ? " or null" : ""}`);
|
|
4151
|
+
}
|
|
4152
|
+
for (const field of ["fee_basis", "included_basis"]) {
|
|
4153
|
+
if (body[field] !== undefined && !["unknown", "observed", "configured"].includes(String(body[field])))
|
|
4154
|
+
return err(`${field} must be unknown, observed or configured`);
|
|
4155
|
+
}
|
|
4156
|
+
if (body["monthly_fee_usd"] === null || body["fee_usd"] === null)
|
|
4157
|
+
return err("monthly_fee_usd must be a non-negative number; use fee_basis unknown for an unknown amount");
|
|
4158
|
+
if (body["included_usage_usd"] === null)
|
|
4159
|
+
return err("included_usage_usd must be a non-negative number; use included_basis unknown for an unknown amount");
|
|
4160
|
+
const monthlyFee = body["monthly_fee_usd"] ?? body["fee_usd"] ?? 0;
|
|
4161
|
+
const includedUsage = body["included_usage_usd"] ?? 0;
|
|
4162
|
+
if (typeof monthlyFee !== "number" || !Number.isFinite(monthlyFee) || monthlyFee < 0)
|
|
4163
|
+
return err("monthly_fee_usd must be a non-negative number");
|
|
4164
|
+
if (typeof includedUsage !== "number" || !Number.isFinite(includedUsage) || includedUsage < 0)
|
|
4165
|
+
return err("included_usage_usd must be a non-negative number");
|
|
4166
|
+
const agent = optionalAgent(body["agent"]);
|
|
4167
|
+
if (agent === undefined)
|
|
4168
|
+
return err(AGENT_ERROR);
|
|
4169
|
+
const now = new Date().toISOString();
|
|
4170
|
+
const accountKey = optionalString(body["account_key"]) ?? "";
|
|
4171
|
+
const observedAt = optionalString(body["observed_at"]) ?? (accountKey ? now : null);
|
|
4172
|
+
const subscription = {
|
|
4173
|
+
id: optionalString(body["id"])?.trim() || (accountKey ? subscriptionId({ provider, account_key: accountKey }) : randomUUID2()),
|
|
4174
|
+
agent,
|
|
4175
|
+
provider,
|
|
4176
|
+
plan,
|
|
4177
|
+
monthly_fee_usd: monthlyFee,
|
|
4178
|
+
included_usage_usd: includedUsage,
|
|
4179
|
+
billing_cycle_start: optionalString(body["billing_cycle_start"]),
|
|
4180
|
+
reset_policy: optionalString(body["reset_policy"]) ?? "unknown",
|
|
4181
|
+
account_key: accountKey,
|
|
4182
|
+
observed_station: optionalString(body["observed_station"]),
|
|
4183
|
+
observed_at: observedAt,
|
|
4184
|
+
fee_basis: body["fee_basis"] ?? "unknown",
|
|
4185
|
+
included_basis: body["included_basis"] ?? "unknown",
|
|
4186
|
+
active: body["active"] === false || body["active"] === 0 ? 0 : 1,
|
|
4187
|
+
created_at: optionalString(body["created_at"]) ?? now,
|
|
4188
|
+
updated_at: observedAt ?? now
|
|
4189
|
+
};
|
|
4190
|
+
upsertSubscription(db, subscription);
|
|
4191
|
+
return ok(db.get("SELECT * FROM subscriptions WHERE id = ?", subscription.id));
|
|
4192
|
+
}
|
|
4193
|
+
const subscriptionMatch = path.match(/^\/api\/subscriptions\/(.+)$/);
|
|
4194
|
+
if (subscriptionMatch && method === "DELETE") {
|
|
4195
|
+
deleteSubscription(db, decodeURIComponent(subscriptionMatch[1]));
|
|
4196
|
+
return ok({ ok: true });
|
|
4197
|
+
}
|
|
4198
|
+
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
4199
|
+
if (sessionRequestsMatch && method === "GET") {
|
|
4200
|
+
const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
|
|
4201
|
+
const session = db.prepare(`SELECT * FROM economy_effective_sessions WHERE id = ? OR id LIKE ?`).get(sessionId, `${sessionId}%`);
|
|
4202
|
+
if (!session)
|
|
4203
|
+
return err("Session not found", 404);
|
|
4204
|
+
const requests = db.prepare(`SELECT * FROM economy_effective_requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
4205
|
+
return ok(requests, { session_id: session["id"], count: requests.length });
|
|
4206
|
+
}
|
|
4207
|
+
if (path === "/api/goals" && method === "GET") {
|
|
4208
|
+
return ok(getGoalStatuses(db));
|
|
4209
|
+
}
|
|
4210
|
+
if (path === "/api/goals" && method === "POST") {
|
|
4211
|
+
const body = await jsonBody(req);
|
|
4212
|
+
if (!body)
|
|
4213
|
+
return err("invalid JSON body");
|
|
4214
|
+
const period = body["period"] ?? "month";
|
|
4215
|
+
if (!["day", "week", "month", "year"].includes(String(period)))
|
|
4216
|
+
return err("period must be day, week, month, or year");
|
|
4217
|
+
const limitUsd = finiteNumber(body["limit_usd"]);
|
|
4218
|
+
if (limitUsd == null || limitUsd <= 0)
|
|
4219
|
+
return err("limit_usd must be a positive number");
|
|
4220
|
+
const agent = optionalAgent(body["agent"]);
|
|
4221
|
+
if (agent === undefined)
|
|
4222
|
+
return err(AGENT_ERROR);
|
|
4223
|
+
const now = new Date().toISOString();
|
|
4224
|
+
const goal = {
|
|
4225
|
+
id: randomUUID2(),
|
|
4226
|
+
period,
|
|
4227
|
+
project_path: optionalString(body["project_path"]),
|
|
4228
|
+
agent,
|
|
4229
|
+
limit_usd: limitUsd,
|
|
4230
|
+
created_at: now,
|
|
4231
|
+
updated_at: now
|
|
4232
|
+
};
|
|
4233
|
+
upsertGoal(db, goal);
|
|
4234
|
+
return ok(getGoalStatuses(db).find((g) => g.id === goal.id) ?? goal);
|
|
4235
|
+
}
|
|
4236
|
+
const goalMatch = path.match(/^\/api\/goals\/(.+)$/);
|
|
4237
|
+
if (goalMatch && method === "DELETE") {
|
|
4238
|
+
deleteGoal(db, decodeURIComponent(goalMatch[1]));
|
|
4239
|
+
return ok({ ok: true });
|
|
4240
|
+
}
|
|
4241
|
+
if (path === "/api/feedback" && method === "POST") {
|
|
4242
|
+
const body = await jsonBody(req);
|
|
4243
|
+
if (!body)
|
|
4244
|
+
return err("invalid JSON body");
|
|
4245
|
+
const message = optionalString(body["message"])?.trim();
|
|
4246
|
+
if (!message)
|
|
4247
|
+
return err("message is required");
|
|
4248
|
+
const email = optionalString(body["email"])?.trim() || null;
|
|
4249
|
+
const rawCategory = optionalString(body["category"])?.trim() || "general";
|
|
4250
|
+
if (!["bug", "feature", "general"].includes(rawCategory))
|
|
4251
|
+
return err("category must be bug, feature, or general");
|
|
4252
|
+
insertFeedback(db, {
|
|
4253
|
+
message,
|
|
4254
|
+
email,
|
|
4255
|
+
category: rawCategory,
|
|
4256
|
+
version: packageMetadata.version,
|
|
4257
|
+
machine_id: getMachineId()
|
|
4258
|
+
});
|
|
4259
|
+
return ok({ ok: true });
|
|
4260
|
+
}
|
|
4261
|
+
return err("Not found", 404);
|
|
4262
|
+
};
|
|
4263
|
+
}
|
|
4264
|
+
async function executeServiceOperation(db, operation) {
|
|
4265
|
+
const request = new Request(`http://economy.invalid${operation.path}${operation.search}`, {
|
|
4266
|
+
method: operation.method,
|
|
4267
|
+
...operation.body === null ? {} : { body: operation.body, headers: { "Content-Type": "application/json" } }
|
|
4268
|
+
});
|
|
4269
|
+
try {
|
|
4270
|
+
const response = await createDataHandler(db, operation.actorId)(request);
|
|
4271
|
+
return { status: response.status, body: await response.text() };
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
if (error instanceof IngestError)
|
|
4274
|
+
return {
|
|
4275
|
+
status: error.status,
|
|
4276
|
+
body: JSON.stringify({ error: error.message, code: error.code, field: error.field, expected: error.expected })
|
|
4277
|
+
};
|
|
4278
|
+
throw error;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
// src/server/service-admission.ts
|
|
4283
|
+
class ServiceAdmission {
|
|
4284
|
+
capacity;
|
|
4285
|
+
deadlineMs;
|
|
4286
|
+
active = 0;
|
|
4287
|
+
constructor(capacity, deadlineMs) {
|
|
4288
|
+
this.capacity = capacity;
|
|
4289
|
+
this.deadlineMs = deadlineMs;
|
|
4290
|
+
}
|
|
4291
|
+
async run(work) {
|
|
4292
|
+
if (this.active >= this.capacity)
|
|
4293
|
+
throw new Error("Economy authority is busy");
|
|
4294
|
+
this.active++;
|
|
4295
|
+
let timer;
|
|
4296
|
+
try {
|
|
4297
|
+
const operation = Promise.resolve().then(work).finally(() => {
|
|
4298
|
+
this.active--;
|
|
4299
|
+
});
|
|
4300
|
+
return await Promise.race([
|
|
4301
|
+
operation,
|
|
4302
|
+
new Promise((_, reject) => {
|
|
4303
|
+
timer = setTimeout(() => reject(new Error("Economy authority timed out")), this.deadlineMs);
|
|
4304
|
+
})
|
|
4305
|
+
]);
|
|
4306
|
+
} finally {
|
|
4307
|
+
clearTimeout(timer);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
// src/server/human-auth.ts
|
|
4313
|
+
var AUTHORITY = "https://api.hasna.com/identities/v1/auth/economy-authority";
|
|
4314
|
+
var identifier = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,255}$/.test(value);
|
|
4315
|
+
function parseHumanAuthBinding(raw) {
|
|
4316
|
+
if (raw === undefined || raw === "")
|
|
4317
|
+
return;
|
|
4318
|
+
try {
|
|
4319
|
+
const v = JSON.parse(raw);
|
|
4320
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
4321
|
+
throw Error();
|
|
4322
|
+
const b = v;
|
|
4323
|
+
if (Object.keys(v).some((k) => !["tenantId", "principals", "authorityUrl"].includes(k)) || !identifier(b.tenantId) || !Array.isArray(b.principals) || !b.principals.length || b.principals.length > 100)
|
|
4324
|
+
throw Error();
|
|
4325
|
+
const seen = new Set;
|
|
4326
|
+
for (const p of b.principals) {
|
|
4327
|
+
if (!p || Object.keys(p).some((k) => !["subject", "scopes"].includes(k)) || !identifier(p.subject) || seen.has(p.subject) || !Array.isArray(p.scopes) || !p.scopes.length || p.scopes.length > 2 || p.scopes.some((s) => s !== "economy:read" && s !== "economy:write"))
|
|
4328
|
+
throw Error();
|
|
4329
|
+
seen.add(p.subject);
|
|
4330
|
+
}
|
|
4331
|
+
const url = new URL(b.authorityUrl ?? AUTHORITY);
|
|
4332
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/identities/v1/auth/economy-authority" && url.pathname !== "/v1/auth/economy-authority")
|
|
4333
|
+
throw Error();
|
|
4334
|
+
return { tenantId: b.tenantId, principals: b.principals.map((p) => ({ subject: p.subject, scopes: [...p.scopes] })), authorityUrl: url.href };
|
|
4335
|
+
} catch {
|
|
4336
|
+
throw new Error("Invalid ECONOMY_HUMAN_AUTH_BINDING; explicit tenant, principals and HTTPS authority are required");
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
function withHumanAuthentication(agent, binding, options = {}) {
|
|
4340
|
+
const config = binding ? parseHumanAuthBinding(JSON.stringify(binding)) : undefined;
|
|
4341
|
+
const fetcher = options.fetch ?? fetch;
|
|
4342
|
+
const now = options.now ?? Date.now;
|
|
4343
|
+
return { async authenticate(headers, context) {
|
|
4344
|
+
const bearer = headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i)?.[1];
|
|
4345
|
+
if (headers.has("x-api-key") || !bearer || bearer.startsWith("hasna_"))
|
|
4346
|
+
return agent.authenticate(headers, context);
|
|
4347
|
+
if (bearer.length > 8192 || bearer.split(".").length !== 3)
|
|
4348
|
+
return { ok: false, status: 401, reason: "invalid_human_token" };
|
|
4349
|
+
if (!config)
|
|
4350
|
+
return { ok: false, status: 503, reason: "human_auth_not_configured" };
|
|
4351
|
+
const controller = new AbortController;
|
|
4352
|
+
let timer;
|
|
4353
|
+
try {
|
|
4354
|
+
const operation = async () => {
|
|
4355
|
+
const response = await fetcher(config.authorityUrl, { method: "GET", headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }, redirect: "error", signal: controller.signal });
|
|
4356
|
+
if (response.status === 401 || response.status === 403) {
|
|
4357
|
+
response.body?.cancel().catch(() => {});
|
|
4358
|
+
return { ok: false, status: response.status, reason: "human_authority_denied" };
|
|
4359
|
+
}
|
|
4360
|
+
if (!response.ok || !response.body) {
|
|
4361
|
+
response.body?.cancel().catch(() => {});
|
|
4362
|
+
throw Error();
|
|
4363
|
+
}
|
|
4364
|
+
const reader = response.body.getReader();
|
|
4365
|
+
const chunks = [];
|
|
4366
|
+
let size = 0;
|
|
4367
|
+
try {
|
|
4368
|
+
for (;; ) {
|
|
4369
|
+
const { value, done } = await reader.read();
|
|
4370
|
+
if (done)
|
|
4371
|
+
break;
|
|
4372
|
+
size += value.byteLength;
|
|
4373
|
+
if (size > 16384)
|
|
4374
|
+
throw Error();
|
|
4375
|
+
chunks.push(value);
|
|
4376
|
+
}
|
|
4377
|
+
} finally {
|
|
4378
|
+
reader.cancel().catch(() => {});
|
|
4379
|
+
}
|
|
4380
|
+
const data = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
4381
|
+
if (data.active !== true || data.issuer !== "identities" || data.audience !== "economy" || data.tenant_id !== config.tenantId || !identifier(data.subject) || !identifier(data.session_id) || !identifier(data.token_id) || !Number.isSafeInteger(data.expires_at) || data.expires_at * 1000 <= now() || data.expires_at * 1000 > now() + 300000 || !Array.isArray(data.scopes) || data.scopes.some((s) => s !== "economy:read" && s !== "economy:write"))
|
|
4382
|
+
return { ok: false, status: 403, reason: "human_binding_denied" };
|
|
4383
|
+
const allowed = config.principals.find((p) => p.subject === data.subject);
|
|
4384
|
+
if (!allowed)
|
|
4385
|
+
return { ok: false, status: 403, reason: "human_binding_denied" };
|
|
4386
|
+
const scopes = allowed.scopes.filter((s) => data.scopes.includes(s));
|
|
4387
|
+
const required = context?.requiredScopes ?? ["economy:read"];
|
|
4388
|
+
if (!scopes.length || !required.every((s) => scopes.includes(s)))
|
|
4389
|
+
return { ok: false, status: 403, reason: "insufficient_scope" };
|
|
4390
|
+
return { ok: true, status: 200, principal: { tid: config.tenantId, kid: `human:${data.subject}`, kind: "human", subject: data.subject, scopes, sessionId: data.session_id, expiresAt: data.expires_at } };
|
|
4391
|
+
};
|
|
4392
|
+
return await Promise.race([operation(), new Promise((_, reject) => {
|
|
4393
|
+
timer = setTimeout(() => {
|
|
4394
|
+
controller.abort();
|
|
4395
|
+
reject(Error());
|
|
4396
|
+
}, 3000);
|
|
4397
|
+
})]);
|
|
4398
|
+
} catch {
|
|
4399
|
+
return { ok: false, status: 503, reason: "human_authority_unavailable" };
|
|
4400
|
+
} finally {
|
|
4401
|
+
clearTimeout(timer);
|
|
4402
|
+
controller.abort();
|
|
4403
|
+
}
|
|
4404
|
+
} };
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
// src/server/serve.ts
|
|
4408
|
+
function openApiDocument() {
|
|
4409
|
+
const spec = openApiSpec;
|
|
4410
|
+
return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
|
|
4411
|
+
}
|
|
4412
|
+
var CORS2 = {
|
|
4413
|
+
"Access-Control-Allow-Origin": "*",
|
|
4414
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
4415
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
|
|
4416
|
+
};
|
|
4417
|
+
function json2(data, status = 200) {
|
|
4418
|
+
return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", ...CORS2 } });
|
|
4419
|
+
}
|
|
4420
|
+
function requiredScopesForRequest(method, path) {
|
|
4421
|
+
if (method === "GET" && path === "/api/auth/session")
|
|
4422
|
+
return [];
|
|
4423
|
+
if (method === "GET" && path === "/api/ingest/receipts")
|
|
4424
|
+
return ["economy:write"];
|
|
4425
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method))
|
|
4426
|
+
return ["economy:write"];
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
async function boundedBody(req, limits) {
|
|
4430
|
+
if (!req.body)
|
|
4431
|
+
return null;
|
|
4432
|
+
const reader = req.body.getReader();
|
|
4433
|
+
const chunks = [];
|
|
4434
|
+
let size = 0;
|
|
4435
|
+
let timer;
|
|
4436
|
+
try {
|
|
4437
|
+
const read = async () => {
|
|
4438
|
+
for (;; ) {
|
|
4439
|
+
const { done, value } = await reader.read();
|
|
4440
|
+
if (done)
|
|
4441
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4442
|
+
size += value.byteLength;
|
|
4443
|
+
if (size > limits.maxInputBytes)
|
|
4444
|
+
throw new ServiceUnavailable("input_too_large", 413, "", "not_started");
|
|
4445
|
+
chunks.push(value);
|
|
4446
|
+
}
|
|
4447
|
+
};
|
|
4448
|
+
return await Promise.race([read(), new Promise((_, reject) => {
|
|
4449
|
+
timer = setTimeout(() => reject(new ServiceUnavailable("input_timeout", 408, "", "not_started")), limits.deadlineMs);
|
|
4450
|
+
})]);
|
|
4451
|
+
} finally {
|
|
4452
|
+
clearTimeout(timer);
|
|
4453
|
+
reader.cancel().catch(() => {});
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
function createHandler(target, options = {}) {
|
|
4457
|
+
const limits = options.limits ?? serviceLimits();
|
|
4458
|
+
const dispatcher = "dispatch" in target ? target : {
|
|
4459
|
+
dispatch: async (operation, _signal, beforeStart) => {
|
|
4460
|
+
await beforeStart?.();
|
|
4461
|
+
return executeServiceOperation(target, operation);
|
|
4462
|
+
}
|
|
4463
|
+
};
|
|
4464
|
+
const authority = new ServiceAdmission(32, Math.min(5000, limits.deadlineMs));
|
|
4465
|
+
let readingBodies = 0;
|
|
4466
|
+
return async function handler(req) {
|
|
4467
|
+
const url = new URL(req.url);
|
|
4468
|
+
const rawPath = url.pathname;
|
|
4469
|
+
const path = rawPath.startsWith("/v1/") ? "/api" + rawPath.slice(3) : rawPath === "/v1" ? "/api" : rawPath;
|
|
4470
|
+
const method = req.method;
|
|
4471
|
+
if (method === "OPTIONS")
|
|
4472
|
+
return new Response(null, { status: 204, headers: CORS2 });
|
|
4473
|
+
if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
|
|
4474
|
+
return json2({ status: "ok", version: packageMetadata.version, backend: resolveEconomyServerBackend() });
|
|
4475
|
+
}
|
|
4476
|
+
if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version"))
|
|
4477
|
+
return json2({ version: packageMetadata.version });
|
|
4478
|
+
if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
|
|
4479
|
+
try {
|
|
4480
|
+
const result = options.readyCheck ? await authority.run(options.readyCheck) : { ready: true };
|
|
4481
|
+
return json2(result.detail ? { ready: result.ready, reason: result.detail } : { ready: result.ready }, result.ready ? 200 : 503);
|
|
4482
|
+
} catch {
|
|
4483
|
+
return json2({ ready: false, reason: "storage_unreachable" }, 503);
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
if (method === "GET" && rawPath === "/openapi.json")
|
|
4487
|
+
return json2(openApiDocument());
|
|
4488
|
+
let actorId = "legacy";
|
|
4489
|
+
let human = false;
|
|
4490
|
+
let beforeStart;
|
|
4491
|
+
if (options.authenticator) {
|
|
4492
|
+
const headers = new Headers(req.headers);
|
|
4493
|
+
const authenticate = () => authority.run(() => options.authenticator.authenticate(headers, {
|
|
4494
|
+
method,
|
|
4495
|
+
path: rawPath,
|
|
4496
|
+
requiredScopes: requiredScopesForRequest(method, path)
|
|
4497
|
+
}));
|
|
4498
|
+
const actor = (decision2) => {
|
|
4499
|
+
const principal = decision2.principal;
|
|
4500
|
+
return principal && (principal.tid === null || typeof principal.tid === "string") && typeof principal.kid === "string" && principal.kid.length > 0 ? JSON.stringify([principal.tid, principal.kid]) : undefined;
|
|
4501
|
+
};
|
|
4502
|
+
let decision;
|
|
4503
|
+
try {
|
|
4504
|
+
decision = await authenticate();
|
|
4505
|
+
} catch {
|
|
4506
|
+
return json2({ error: "authentication_unavailable" }, 503);
|
|
4507
|
+
}
|
|
4508
|
+
if (!decision.ok)
|
|
4509
|
+
return json2({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
|
|
4510
|
+
human = decision.principal?.kind === "human";
|
|
4511
|
+
if (method === "GET" && path === "/api/auth/session") {
|
|
4512
|
+
const p = decision.principal;
|
|
4513
|
+
if (p?.kind !== "human")
|
|
4514
|
+
return json2({ error: "human_principal_required" }, 403);
|
|
4515
|
+
const response = json2({ data: { subject: p.subject, tenant_id: p.tid, scopes: p.scopes, session_id: p.sessionId, expires_at: p.expiresAt }, meta: {} });
|
|
4516
|
+
response.headers.set("Cache-Control", "no-store");
|
|
4517
|
+
return response;
|
|
4518
|
+
}
|
|
4519
|
+
const binding = actor(decision);
|
|
4520
|
+
if (binding !== undefined)
|
|
4521
|
+
actorId = binding;
|
|
4522
|
+
else if (method === "POST" && path === "/api/ingest" || path === "/api/ingest/receipts")
|
|
4523
|
+
return json2({ error: "authenticated_principal_required" }, 503);
|
|
4524
|
+
beforeStart = async () => {
|
|
4525
|
+
const fresh = await authenticate();
|
|
4526
|
+
if (!fresh.ok)
|
|
4527
|
+
throw new ServiceUnavailable(fresh.reason ?? "unauthorized", fresh.status || 401, "", "not_started");
|
|
4528
|
+
if (actor(fresh) !== binding)
|
|
4529
|
+
throw new ServiceUnavailable("principal_changed", 403, "", "not_started");
|
|
4530
|
+
};
|
|
4531
|
+
} else {
|
|
4532
|
+
if (path === "/api/ingest/receipts")
|
|
4533
|
+
return json2({ error: "authenticated_principal_required" }, 401);
|
|
4534
|
+
if (!isAuthorizedRequest(req, path))
|
|
4535
|
+
return json2({ error: "Unauthorized" }, 401);
|
|
4536
|
+
beforeStart = async () => {
|
|
4537
|
+
if (!isAuthorizedRequest(req, path))
|
|
4538
|
+
throw new ServiceUnavailable("unauthorized", 401, "", "not_started");
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
if (path === "/health")
|
|
4542
|
+
return json2({ data: { status: "ok", ts: new Date().toISOString() }, meta: {} });
|
|
4543
|
+
const route = serviceRoute(method, path);
|
|
4544
|
+
if (!route)
|
|
4545
|
+
return json2({ error: "Not found" }, 404);
|
|
4546
|
+
if (path.length + url.search.length > 8192)
|
|
4547
|
+
return json2({ error: "input_too_large" }, 413);
|
|
4548
|
+
try {
|
|
4549
|
+
let body = null;
|
|
4550
|
+
if (req.body) {
|
|
4551
|
+
if (readingBodies >= limits.workers + limits.maxQueue)
|
|
4552
|
+
return json2({ error: "service_busy", outcome: "not_started" }, 503);
|
|
4553
|
+
readingBodies++;
|
|
4554
|
+
try {
|
|
4555
|
+
body = await boundedBody(req, limits);
|
|
4556
|
+
} finally {
|
|
4557
|
+
readingBodies--;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
const result = await dispatcher.dispatch({ route, method, path, search: url.search, body, actorId }, req.signal, beforeStart);
|
|
4561
|
+
return new Response(result.body, { status: result.status, headers: { "Content-Type": "application/json", ...CORS2, ...human ? { "Cache-Control": "no-store" } : {} } });
|
|
4562
|
+
} catch (error) {
|
|
4563
|
+
if (error instanceof ServiceUnavailable)
|
|
4564
|
+
return json2({ error: error.code, operation_id: error.operationId || undefined, outcome: error.outcome }, error.status);
|
|
4565
|
+
return json2({ error: "operation_failed", outcome: "unknown" }, 503);
|
|
4566
|
+
}
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
function isLocalHost(host) {
|
|
4570
|
+
return ["127.0.0.1", "localhost", "::1"].includes(host);
|
|
4571
|
+
}
|
|
4572
|
+
function startServer(port = 3456, options = {}) {
|
|
4573
|
+
const cloud = options.db ? false : isPostgresBackend();
|
|
4574
|
+
const hostname = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
|
|
4575
|
+
const log = options.log ?? console.log;
|
|
4576
|
+
const limits = serviceLimits();
|
|
4577
|
+
const handlerOptions = { limits };
|
|
4578
|
+
let pool;
|
|
4579
|
+
let dispatcher;
|
|
4580
|
+
if (cloud) {
|
|
4581
|
+
const signingSecret = resolveSigningSecret();
|
|
4582
|
+
if (!signingSecret && !isLocalHost(hostname))
|
|
4583
|
+
throw new Error("economy-serve on a non-local host requires an API signing secret (HASNA_ECONOMY_API_SIGNING_KEY / API_KEY_SIGNING_SECRET)");
|
|
4584
|
+
const max = Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5");
|
|
4585
|
+
if (!Number.isSafeInteger(max) || max < 1 || max > 32)
|
|
4586
|
+
throw new Error("Invalid ECONOMY_PG_POOL_MAX");
|
|
4587
|
+
const timeout = Math.min(5000, limits.deadlineMs);
|
|
4588
|
+
pool = createCloudPool(undefined, { max, connectionTimeoutMillis: timeout, query_timeout: timeout, statement_timeout: timeout });
|
|
4589
|
+
const authPool = pool;
|
|
4590
|
+
if (signingSecret) {
|
|
4591
|
+
const keys = new ApiKeyStore(authClientFromPool(authPool));
|
|
4592
|
+
keys.ensureSchema().catch(() => log("api_keys ensureSchema failed"));
|
|
4593
|
+
handlerOptions.authenticator = withHumanAuthentication(verifyApiKey({
|
|
4594
|
+
app: "economy",
|
|
4595
|
+
signingSecret,
|
|
4596
|
+
keyStatus: keys.keyStatus,
|
|
4597
|
+
audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
|
|
4598
|
+
}), parseHumanAuthBinding(process.env["ECONOMY_HUMAN_AUTH_BINDING"]));
|
|
4599
|
+
}
|
|
4600
|
+
handlerOptions.readyCheck = async () => {
|
|
4601
|
+
try {
|
|
4602
|
+
const res = await authPool.query("SELECT to_regclass($1) AS reg", ["public.requests"]);
|
|
4603
|
+
return res.rows[0]?.reg ? { ready: true } : { ready: false, detail: "pending_migrations:requests" };
|
|
4604
|
+
} catch {
|
|
4605
|
+
return { ready: false, detail: "storage_unreachable" };
|
|
4606
|
+
}
|
|
4607
|
+
};
|
|
4608
|
+
dispatcher = new AsyncServiceDispatcher({ kind: "postgresql", dsn: getCloudDatabaseUrl() }, limits);
|
|
4609
|
+
} else if (!options.db) {
|
|
4610
|
+
dispatcher = new AsyncServiceDispatcher({ kind: "sqlite" }, limits);
|
|
4611
|
+
} else {
|
|
4612
|
+
ensurePricingSeeded(options.db);
|
|
4613
|
+
}
|
|
4614
|
+
const apiHandler = createHandler(dispatcher ?? options.db, handlerOptions);
|
|
4615
|
+
let server;
|
|
4616
|
+
try {
|
|
4617
|
+
server = Bun.serve({ port, hostname, fetch: apiHandler, maxRequestBodySize: limits.maxInputBytes });
|
|
4618
|
+
} catch (error) {
|
|
4619
|
+
dispatcher?.close();
|
|
4620
|
+
pool?.end();
|
|
4621
|
+
throw error;
|
|
4622
|
+
}
|
|
4623
|
+
const stop = server.stop.bind(server);
|
|
4624
|
+
let stopping;
|
|
4625
|
+
server.stop = (closeActiveConnections) => {
|
|
4626
|
+
if (closeActiveConnections) {
|
|
4627
|
+
stop(true);
|
|
4628
|
+
dispatcher?.close().catch(() => {});
|
|
4629
|
+
}
|
|
4630
|
+
if (!stopping) {
|
|
4631
|
+
let timer;
|
|
4632
|
+
const drain = (async () => {
|
|
4633
|
+
await stop(closeActiveConnections);
|
|
4634
|
+
await dispatcher?.close();
|
|
4635
|
+
await pool?.end();
|
|
4636
|
+
})();
|
|
4637
|
+
stopping = Promise.race([drain, new Promise((_, reject) => {
|
|
4638
|
+
timer = setTimeout(() => {
|
|
4639
|
+
stop(true);
|
|
4640
|
+
dispatcher?.close().catch(() => {});
|
|
4641
|
+
reject(new Error("Economy server shutdown timed out"));
|
|
4642
|
+
}, limits.deadlineMs + 5000);
|
|
4643
|
+
})]).finally(() => clearTimeout(timer));
|
|
4644
|
+
}
|
|
4645
|
+
return stopping;
|
|
4646
|
+
};
|
|
4647
|
+
const address = `http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${server.port}`;
|
|
4648
|
+
log(`economy-serve listening on ${address}`);
|
|
4649
|
+
return server;
|
|
4650
|
+
}
|
|
4651
|
+
export {
|
|
4652
|
+
startServer,
|
|
4653
|
+
openApiDocument,
|
|
4654
|
+
createHandler
|
|
4655
|
+
};
|