@gethelio/proxy 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +123 -16
- package/dist/index.d.ts +61 -3
- package/dist/index.js +104 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4464,6 +4464,8 @@ function clampInt(value, fallback, min, max) {
|
|
|
4464
4464
|
|
|
4465
4465
|
// src/audit/store.ts
|
|
4466
4466
|
var DRIFT_EVENT_DECISIONS_SQL = "('tool_drift', 'tool_drift_reverted')";
|
|
4467
|
+
var EXPORT_MAX_RECORDS = 1e4;
|
|
4468
|
+
var LIST_MAX_PAGE_SIZE = 1e3;
|
|
4467
4469
|
var CREATE_TABLE_DDL = `
|
|
4468
4470
|
CREATE TABLE IF NOT EXISTS audit_records (
|
|
4469
4471
|
id TEXT PRIMARY KEY,
|
|
@@ -4767,15 +4769,28 @@ var AuditStore = class {
|
|
|
4767
4769
|
const row = this.db.prepare("SELECT * FROM audit_records WHERE id = ?").get(id);
|
|
4768
4770
|
return row ? deserializeRow(row) : void 0;
|
|
4769
4771
|
}
|
|
4770
|
-
/** Query records with filters and pagination. */
|
|
4772
|
+
/** Query records with filters and pagination. Capped at 1,000 per page. */
|
|
4771
4773
|
list(filters = {}, pagination = {}) {
|
|
4772
|
-
const
|
|
4773
|
-
const limit = clamp(pagination.limit ?? 50, 1, 1e3);
|
|
4774
|
+
const limit = clamp(pagination.limit ?? 50, 1, LIST_MAX_PAGE_SIZE);
|
|
4774
4775
|
const offset = Math.max(pagination.offset ?? 0, 0);
|
|
4775
4776
|
const order = pagination.order === "asc" ? "ASC" : "DESC";
|
|
4777
|
+
return this.query(filters, limit, offset, order);
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* Query records for bulk export. Unlike `list()`, which enforces the
|
|
4781
|
+
* dashboard's 1,000-row page cap, this path allows up to
|
|
4782
|
+
* {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
|
|
4783
|
+
* (ascending `created_at`), so a capped export keeps the earliest records.
|
|
4784
|
+
*/
|
|
4785
|
+
listForExport(filters = {}, limit) {
|
|
4786
|
+
const clamped = clamp(limit ?? EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS);
|
|
4787
|
+
return this.query(filters, clamped, 0, "ASC");
|
|
4788
|
+
}
|
|
4789
|
+
query(filters, limit, offset, order) {
|
|
4790
|
+
const { clause, params } = buildWhereClause(filters);
|
|
4776
4791
|
const { total } = this.db.prepare(`SELECT COUNT(*) as total FROM audit_records ${clause}`).get(...params);
|
|
4777
4792
|
const rows = this.db.prepare(
|
|
4778
|
-
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order} LIMIT ? OFFSET ?`
|
|
4793
|
+
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order}, rowid ${order} LIMIT ? OFFSET ?`
|
|
4779
4794
|
).all(...params, limit, offset);
|
|
4780
4795
|
return {
|
|
4781
4796
|
records: rows.map(deserializeRow),
|
|
@@ -5592,6 +5607,7 @@ var MAX_PENDING_BYTES = 64 * 1024 * 1024;
|
|
|
5592
5607
|
var MAX_SENDER_KEYS = 5e4;
|
|
5593
5608
|
var MAX_EVIDENCE_ENTRIES = 16;
|
|
5594
5609
|
var MAX_EVIDENCE_BYTES = 64 * 1024;
|
|
5610
|
+
var MAX_VERSION_LOG_LINES_PER_ORIGIN = 5;
|
|
5595
5611
|
var SWEEP_INTERVAL_MS2 = 3e4;
|
|
5596
5612
|
var GovernanceService = class {
|
|
5597
5613
|
policy;
|
|
@@ -5609,6 +5625,13 @@ var GovernanceService = class {
|
|
|
5609
5625
|
maxSenderKeys;
|
|
5610
5626
|
/** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
|
|
5611
5627
|
senderKeys = /* @__PURE__ */ new Set();
|
|
5628
|
+
/**
|
|
5629
|
+
* Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
|
|
5630
|
+
* the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
|
|
5631
|
+
* other path updates existing entries and skips unknown origins, so the
|
|
5632
|
+
* registry shares the origin cap instead of adding a second growth vector.
|
|
5633
|
+
*/
|
|
5634
|
+
adapters = /* @__PURE__ */ new Map();
|
|
5612
5635
|
pending = /* @__PURE__ */ new Map();
|
|
5613
5636
|
tombstones = /* @__PURE__ */ new Map();
|
|
5614
5637
|
caches = /* @__PURE__ */ new Map();
|
|
@@ -5666,6 +5689,7 @@ var GovernanceService = class {
|
|
|
5666
5689
|
return { status: 503, body: { error: "evaluation_backlog_full" } };
|
|
5667
5690
|
}
|
|
5668
5691
|
const cache = this.cacheFor(req.origin);
|
|
5692
|
+
this.recordAdapterSeen(req.origin, req.adapter_version);
|
|
5669
5693
|
const toolName = req.tool.name;
|
|
5670
5694
|
const hasDefinition = definitionProvided(req.tool);
|
|
5671
5695
|
if (hasDefinition) {
|
|
@@ -5924,6 +5948,7 @@ var GovernanceService = class {
|
|
|
5924
5948
|
finalizedBy: "audit",
|
|
5925
5949
|
expiresAtMs: this.now() + this.ttlMs
|
|
5926
5950
|
});
|
|
5951
|
+
this.touchAdapter(entry.origin);
|
|
5927
5952
|
const body = { ok: true, audit_record_id: auditId };
|
|
5928
5953
|
if (evidenceOutcomes) body["evidence"] = evidenceOutcomes;
|
|
5929
5954
|
return { status: 201, body };
|
|
@@ -5989,6 +6014,7 @@ var GovernanceService = class {
|
|
|
5989
6014
|
if (reserved) {
|
|
5990
6015
|
return { status: 400, body: { error: "reserved_metadata_key", key: reserved } };
|
|
5991
6016
|
}
|
|
6017
|
+
this.touchAdapter(req.origin);
|
|
5992
6018
|
const evaluationId = randomUUID4();
|
|
5993
6019
|
const toolName = `install:${req.package.source ?? "pkg"}:${req.package.name}`;
|
|
5994
6020
|
const verdict = this.evaluateInstall(req);
|
|
@@ -6053,6 +6079,65 @@ var GovernanceService = class {
|
|
|
6053
6079
|
};
|
|
6054
6080
|
}
|
|
6055
6081
|
// -------------------------------------------------------------------------
|
|
6082
|
+
// Adapter liveness registry (issue #126)
|
|
6083
|
+
// -------------------------------------------------------------------------
|
|
6084
|
+
/** Wire-ready liveness entries, most recently seen first. */
|
|
6085
|
+
listAdapters() {
|
|
6086
|
+
return [...this.adapters.entries()].sort(([oa, a], [ob, b]) => b.lastSeenMs - a.lastSeenMs || oa.localeCompare(ob)).map(([origin, state]) => ({
|
|
6087
|
+
origin,
|
|
6088
|
+
adapter_version: state.adapterVersion,
|
|
6089
|
+
first_seen: new Date(state.firstSeenMs).toISOString(),
|
|
6090
|
+
last_seen: new Date(state.lastSeenMs).toISOString()
|
|
6091
|
+
}));
|
|
6092
|
+
}
|
|
6093
|
+
/** Insert-or-refresh on the /evaluate path (the only insert site). */
|
|
6094
|
+
recordAdapterSeen(origin, version) {
|
|
6095
|
+
const normalized = version && version.length <= 64 ? version : void 0;
|
|
6096
|
+
const now = this.now();
|
|
6097
|
+
const existing = this.adapters.get(origin);
|
|
6098
|
+
if (!existing) {
|
|
6099
|
+
const state = {
|
|
6100
|
+
adapterVersion: normalized ?? null,
|
|
6101
|
+
firstSeenMs: now,
|
|
6102
|
+
lastSeenMs: now,
|
|
6103
|
+
versionLogCount: 0
|
|
6104
|
+
};
|
|
6105
|
+
this.adapters.set(origin, state);
|
|
6106
|
+
if (normalized !== void 0) this.logVersionEvent(origin, state, null, normalized);
|
|
6107
|
+
return;
|
|
6108
|
+
}
|
|
6109
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, now);
|
|
6110
|
+
if (normalized !== void 0 && normalized !== existing.adapterVersion) {
|
|
6111
|
+
this.logVersionEvent(origin, existing, existing.adapterVersion, normalized);
|
|
6112
|
+
existing.adapterVersion = normalized;
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
/** Refresh-only for paths without an origin budget gate (install-scan, audit). */
|
|
6116
|
+
touchAdapter(origin) {
|
|
6117
|
+
const existing = this.adapters.get(origin);
|
|
6118
|
+
if (!existing) return;
|
|
6119
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, this.now());
|
|
6120
|
+
}
|
|
6121
|
+
/**
|
|
6122
|
+
* Log a version sighting/change, capped per origin per boot. Both origin and
|
|
6123
|
+
* version are caller-controlled free text, so both are JSON-escaped — a
|
|
6124
|
+
* newline or control character must not be able to forge extra log lines
|
|
6125
|
+
* (the route's origin regex does not protect direct embedders).
|
|
6126
|
+
*/
|
|
6127
|
+
logVersionEvent(origin, state, from, to) {
|
|
6128
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) return;
|
|
6129
|
+
state.versionLogCount += 1;
|
|
6130
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) {
|
|
6131
|
+
console.error(
|
|
6132
|
+
`[helio] adapter origin ${JSON.stringify(origin)}: suppressing further version logs after ${String(MAX_VERSION_LOG_LINES_PER_ORIGIN)}`
|
|
6133
|
+
);
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
console.error(
|
|
6137
|
+
from === null ? `[helio] adapter origin ${JSON.stringify(origin)} reports version ${JSON.stringify(to)}` : `[helio] adapter origin ${JSON.stringify(origin)} version changed ${JSON.stringify(from)} -> ${JSON.stringify(to)}`
|
|
6138
|
+
);
|
|
6139
|
+
}
|
|
6140
|
+
// -------------------------------------------------------------------------
|
|
6056
6141
|
// POST /approval/:id/resolve
|
|
6057
6142
|
// -------------------------------------------------------------------------
|
|
6058
6143
|
resolveApproval(ticketId, req) {
|
|
@@ -6154,6 +6239,7 @@ var GovernanceService = class {
|
|
|
6154
6239
|
this.tombstones.clear();
|
|
6155
6240
|
this.caches.clear();
|
|
6156
6241
|
this.senderKeys.clear();
|
|
6242
|
+
this.adapters.clear();
|
|
6157
6243
|
this.pendingBytes = 0;
|
|
6158
6244
|
}
|
|
6159
6245
|
// -------------------------------------------------------------------------
|
|
@@ -7438,7 +7524,10 @@ var CSV_HEADERS = [
|
|
|
7438
7524
|
"dry_run",
|
|
7439
7525
|
"created_at",
|
|
7440
7526
|
"environment",
|
|
7441
|
-
"matched_rule_index"
|
|
7527
|
+
"matched_rule_index",
|
|
7528
|
+
"record_kind",
|
|
7529
|
+
"origin",
|
|
7530
|
+
"metadata"
|
|
7442
7531
|
];
|
|
7443
7532
|
var FORMULA_PREFIXES = /^[=+\-@\t\r]/;
|
|
7444
7533
|
function csvEscape(value) {
|
|
@@ -7584,7 +7673,7 @@ var feedQuerySchema = z8.object({
|
|
|
7584
7673
|
});
|
|
7585
7674
|
var auditExportQuerySchema = z8.object({
|
|
7586
7675
|
format: z8.preprocess((value) => value === "csv" ? "csv" : "json", z8.enum(["json", "csv"])),
|
|
7587
|
-
limit: clampedQueryInt(
|
|
7676
|
+
limit: clampedQueryInt(EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS),
|
|
7588
7677
|
tool: optionalQueryString,
|
|
7589
7678
|
decision: optionalQueryString,
|
|
7590
7679
|
reason: optionalQueryString,
|
|
@@ -7602,7 +7691,7 @@ var auditExportQuerySchema = z8.object({
|
|
|
7602
7691
|
sender_id: optionalQueryString
|
|
7603
7692
|
});
|
|
7604
7693
|
var auditQuerySchema = z8.object({
|
|
7605
|
-
limit: clampedQueryInt(50, 1,
|
|
7694
|
+
limit: clampedQueryInt(50, 1, LIST_MAX_PAGE_SIZE),
|
|
7606
7695
|
offset: clampedQueryInt(0, 0, Number.MAX_SAFE_INTEGER),
|
|
7607
7696
|
tool: optionalQueryString,
|
|
7608
7697
|
decision: optionalQueryString,
|
|
@@ -7692,7 +7781,8 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7692
7781
|
rateLimiter,
|
|
7693
7782
|
spendLimiter,
|
|
7694
7783
|
evidenceStore,
|
|
7695
|
-
eventBus
|
|
7784
|
+
eventBus,
|
|
7785
|
+
adapterLiveness
|
|
7696
7786
|
} = deps;
|
|
7697
7787
|
const apiSecret = options?.apiSecret;
|
|
7698
7788
|
const sessionStore = apiSecret ? new DashboardSessionStore({ secret: apiSecret, ttlMs: SESSION_TTL_MS }) : void 0;
|
|
@@ -7847,7 +7937,7 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7847
7937
|
channel_id: query.channel_id,
|
|
7848
7938
|
sender_id: query.sender_id
|
|
7849
7939
|
};
|
|
7850
|
-
const result = auditStore.
|
|
7940
|
+
const result = auditStore.listForExport(filters, limit);
|
|
7851
7941
|
if (format === "csv") {
|
|
7852
7942
|
const csv = recordsToCsv(result.records);
|
|
7853
7943
|
return new Response(csv, {
|
|
@@ -7911,6 +8001,9 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7911
8001
|
spend_limits: spendLimiter.listKeyStates()
|
|
7912
8002
|
});
|
|
7913
8003
|
});
|
|
8004
|
+
app.get("/api/adapters", (c) => {
|
|
8005
|
+
return c.json({ adapters: adapterLiveness?.listAdapters() ?? [] });
|
|
8006
|
+
});
|
|
7914
8007
|
app.get("/api/analytics", (c) => {
|
|
7915
8008
|
const query = analyticsQuerySchema.parse(c.req.query());
|
|
7916
8009
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -8441,6 +8534,7 @@ async function startCommand(configPath, options) {
|
|
|
8441
8534
|
let sidebandToken;
|
|
8442
8535
|
let sidebandTokenSource;
|
|
8443
8536
|
let adapterToken;
|
|
8537
|
+
let adapterTokenSource;
|
|
8444
8538
|
let governanceService;
|
|
8445
8539
|
if (config.sdk.enabled) {
|
|
8446
8540
|
sidebandToken = process.env["HELIO_SDK_TOKEN"];
|
|
@@ -8455,6 +8549,9 @@ async function startCommand(configPath, options) {
|
|
|
8455
8549
|
if (!adapterToken || adapterToken.length === 0) {
|
|
8456
8550
|
adapterToken = randomBytes2(32).toString("hex");
|
|
8457
8551
|
process.env["HELIO_ADAPTER_TOKEN"] = adapterToken;
|
|
8552
|
+
adapterTokenSource = "generated";
|
|
8553
|
+
} else {
|
|
8554
|
+
adapterTokenSource = "env";
|
|
8458
8555
|
}
|
|
8459
8556
|
governanceService = new GovernanceService({
|
|
8460
8557
|
policy,
|
|
@@ -8485,7 +8582,10 @@ async function startCommand(configPath, options) {
|
|
|
8485
8582
|
rateLimiter,
|
|
8486
8583
|
spendLimiter,
|
|
8487
8584
|
evidenceStore,
|
|
8488
|
-
eventBus
|
|
8585
|
+
eventBus,
|
|
8586
|
+
// Adapter liveness for GET /api/adapters (issue #126); undefined
|
|
8587
|
+
// unless the SDK sideband is enabled → endpoint serves an empty list.
|
|
8588
|
+
adapterLiveness: governanceService
|
|
8489
8589
|
},
|
|
8490
8590
|
{
|
|
8491
8591
|
apiSecret: config.dashboard.api_secret,
|
|
@@ -8517,15 +8617,14 @@ async function startCommand(configPath, options) {
|
|
|
8517
8617
|
if (sidebandHandle) {
|
|
8518
8618
|
console.error(`SDK sideband listening on http://${config.sdk.host}:${String(config.sdk.port)}`);
|
|
8519
8619
|
if (sidebandToken) {
|
|
8520
|
-
const source = sidebandTokenSource === "env" ? "reusing HELIO_SDK_TOKEN from environment" : "generated per-boot HELIO_SDK_TOKEN";
|
|
8521
8620
|
console.error(
|
|
8522
|
-
`SDK token (
|
|
8621
|
+
sidebandTokenSource === "env" ? "SDK token: reusing HELIO_SDK_TOKEN from environment (value not shown)" : `SDK token (generated per-boot HELIO_SDK_TOKEN; pass as HELIO_SDK_TOKEN env var to your SDK clients):
|
|
8523
8622
|
${sidebandToken}`
|
|
8524
8623
|
);
|
|
8525
8624
|
}
|
|
8526
8625
|
if (adapterToken) {
|
|
8527
8626
|
console.error(
|
|
8528
|
-
`Adapter token (governance routes; pass as HELIO_ADAPTER_TOKEN to your adapter):
|
|
8627
|
+
adapterTokenSource === "env" ? "Adapter token: reusing HELIO_ADAPTER_TOKEN from environment (value not shown)" : `Adapter token (generated per-boot HELIO_ADAPTER_TOKEN; governance routes; pass as HELIO_ADAPTER_TOKEN to your adapter):
|
|
8529
8628
|
${adapterToken}`
|
|
8530
8629
|
);
|
|
8531
8630
|
}
|
|
@@ -8656,6 +8755,14 @@ async function validateCommand(configPath) {
|
|
|
8656
8755
|
}
|
|
8657
8756
|
}
|
|
8658
8757
|
async function exportCommand(opts) {
|
|
8758
|
+
const parsedLimit = Number(opts.limit);
|
|
8759
|
+
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
|
8760
|
+
console.error(
|
|
8761
|
+
`Error: --limit must be an integer between 1 and ${String(EXPORT_MAX_RECORDS)} (got "${opts.limit}")`
|
|
8762
|
+
);
|
|
8763
|
+
process.exit(1);
|
|
8764
|
+
}
|
|
8765
|
+
const limit = Math.min(parsedLimit, EXPORT_MAX_RECORDS);
|
|
8659
8766
|
let config;
|
|
8660
8767
|
try {
|
|
8661
8768
|
config = await loadConfig(opts.config);
|
|
@@ -8674,7 +8781,7 @@ async function exportCommand(opts) {
|
|
|
8674
8781
|
// No cleanup timer for one-shot CLI
|
|
8675
8782
|
});
|
|
8676
8783
|
try {
|
|
8677
|
-
const result = store.
|
|
8784
|
+
const result = store.listForExport(
|
|
8678
8785
|
{
|
|
8679
8786
|
tool_name: opts.tool,
|
|
8680
8787
|
policy_decision: opts.decision,
|
|
@@ -8683,7 +8790,7 @@ async function exportCommand(opts) {
|
|
|
8683
8790
|
from: opts.from,
|
|
8684
8791
|
to: opts.to
|
|
8685
8792
|
},
|
|
8686
|
-
|
|
8793
|
+
limit
|
|
8687
8794
|
);
|
|
8688
8795
|
if (opts.format === "csv") {
|
|
8689
8796
|
writeCsv(result.records);
|
|
@@ -8755,5 +8862,5 @@ program.command("start").description("Load config and start the proxy server").o
|
|
|
8755
8862
|
);
|
|
8756
8863
|
program.command("init").description("Scaffold a helio.yaml config file with commented defaults").option("-o, --output <path>", "Output file path", DEFAULT_CONFIG_PATH).option("-f, --force", "Overwrite existing file", false).action((opts) => initCommand(opts.output, opts.force));
|
|
8757
8864
|
program.command("validate").description("Validate a helio.yaml config file").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).action((opts) => validateCommand(opts.config));
|
|
8758
|
-
program.command("export").description("Export audit records to JSON or CSV").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).option("-f, --format <format>", "Output format: json or csv", "json").option("--tool <name>", "Filter by tool name").option("--decision <decision>", "Filter by policy decision").option("--reason <reason>", "Filter by block reason").option("--session <id>", "Filter by session ID").option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max records to export", "1000").action((opts) => exportCommand(opts));
|
|
8865
|
+
program.command("export").description("Export audit records to JSON or CSV").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).option("-f, --format <format>", "Output format: json or csv", "json").option("--tool <name>", "Filter by tool name").option("--decision <decision>", "Filter by policy decision").option("--reason <reason>", "Filter by block reason").option("--session <id>", "Filter by session ID").option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max records to export (up to 10000)", "1000").action((opts) => exportCommand(opts));
|
|
8759
8866
|
program.parse();
|
package/dist/index.d.ts
CHANGED
|
@@ -921,7 +921,7 @@ interface AuditQueryFilters {
|
|
|
921
921
|
}
|
|
922
922
|
/** Pagination options for list queries. */
|
|
923
923
|
interface AuditPaginationOptions {
|
|
924
|
-
/** Maximum number of records to return (default: 50, max:
|
|
924
|
+
/** Maximum number of records to return (default: 50, max: 1,000 — `LIST_MAX_PAGE_SIZE`). */
|
|
925
925
|
readonly limit?: number;
|
|
926
926
|
/** Number of records to skip (default: 0). */
|
|
927
927
|
readonly offset?: number;
|
|
@@ -986,6 +986,17 @@ interface AuditStoreOptions {
|
|
|
986
986
|
readonly cleanupIntervalMs?: number;
|
|
987
987
|
}
|
|
988
988
|
|
|
989
|
+
/**
|
|
990
|
+
* Maximum records a single bulk export may return. Shared by the dashboard
|
|
991
|
+
* export route schema and the CLI export command so the advertised cap and
|
|
992
|
+
* the store's actual cap cannot diverge.
|
|
993
|
+
*/
|
|
994
|
+
declare const EXPORT_MAX_RECORDS = 10000;
|
|
995
|
+
/**
|
|
996
|
+
* Maximum page size for paginated `list()` reads. Shared with the dashboard
|
|
997
|
+
* audit route schema for the same reason as {@link EXPORT_MAX_RECORDS}.
|
|
998
|
+
*/
|
|
999
|
+
declare const LIST_MAX_PAGE_SIZE = 1000;
|
|
989
1000
|
/**
|
|
990
1001
|
* SQLite-backed audit record store.
|
|
991
1002
|
*
|
|
@@ -1032,8 +1043,16 @@ declare class AuditStore {
|
|
|
1032
1043
|
insertBatch(records: ReadonlyArray<Omit<AuditRecord, 'id' | 'created_at'>>, onError?: (record: Omit<AuditRecord, 'id' | 'created_at'>, err: unknown) => void, ids?: ReadonlyArray<string>, onPersist?: (record: Omit<AuditRecord, 'id' | 'created_at'>, id: string) => void): number;
|
|
1033
1044
|
/** Get a single record by ID, or undefined if not found. */
|
|
1034
1045
|
get(id: string): AuditRecord | undefined;
|
|
1035
|
-
/** Query records with filters and pagination. */
|
|
1046
|
+
/** Query records with filters and pagination. Capped at 1,000 per page. */
|
|
1036
1047
|
list(filters?: AuditQueryFilters, pagination?: AuditPaginationOptions): AuditListResult;
|
|
1048
|
+
/**
|
|
1049
|
+
* Query records for bulk export. Unlike `list()`, which enforces the
|
|
1050
|
+
* dashboard's 1,000-row page cap, this path allows up to
|
|
1051
|
+
* {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
|
|
1052
|
+
* (ascending `created_at`), so a capped export keeps the earliest records.
|
|
1053
|
+
*/
|
|
1054
|
+
listForExport(filters?: AuditQueryFilters, limit?: number): AuditListResult;
|
|
1055
|
+
private query;
|
|
1037
1056
|
/** Count records matching the given filters. */
|
|
1038
1057
|
count(filters?: AuditQueryFilters): number;
|
|
1039
1058
|
/** Get aggregate statistics for a time range. */
|
|
@@ -1962,6 +1981,17 @@ interface ServiceResult {
|
|
|
1962
1981
|
readonly status: number;
|
|
1963
1982
|
readonly body: Record<string, unknown>;
|
|
1964
1983
|
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Per-origin adapter liveness, wire-ready for the dashboard's
|
|
1986
|
+
* `GET /api/adapters` (issue #126). ISO-8601 timestamps; `adapter_version`
|
|
1987
|
+
* stays null until an /evaluate supplies one.
|
|
1988
|
+
*/
|
|
1989
|
+
interface AdapterLivenessEntry {
|
|
1990
|
+
readonly origin: string;
|
|
1991
|
+
readonly adapter_version: string | null;
|
|
1992
|
+
readonly first_seen: string;
|
|
1993
|
+
readonly last_seen: string;
|
|
1994
|
+
}
|
|
1965
1995
|
interface GovernanceServiceOptions {
|
|
1966
1996
|
readonly policy: CompiledPolicy;
|
|
1967
1997
|
readonly environment?: string;
|
|
@@ -2001,6 +2031,13 @@ declare class GovernanceService {
|
|
|
2001
2031
|
private readonly maxSenderKeys;
|
|
2002
2032
|
/** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
|
|
2003
2033
|
private readonly senderKeys;
|
|
2034
|
+
/**
|
|
2035
|
+
* Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
|
|
2036
|
+
* the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
|
|
2037
|
+
* other path updates existing entries and skips unknown origins, so the
|
|
2038
|
+
* registry shares the origin cap instead of adding a second growth vector.
|
|
2039
|
+
*/
|
|
2040
|
+
private readonly adapters;
|
|
2004
2041
|
private readonly pending;
|
|
2005
2042
|
private readonly tombstones;
|
|
2006
2043
|
private readonly caches;
|
|
@@ -2031,6 +2068,19 @@ declare class GovernanceService {
|
|
|
2031
2068
|
installScan(req: InstallScanInput): ServiceResult;
|
|
2032
2069
|
/** First-match-wins evaluation of the compiled install policy (issue #13). */
|
|
2033
2070
|
private evaluateInstall;
|
|
2071
|
+
/** Wire-ready liveness entries, most recently seen first. */
|
|
2072
|
+
listAdapters(): AdapterLivenessEntry[];
|
|
2073
|
+
/** Insert-or-refresh on the /evaluate path (the only insert site). */
|
|
2074
|
+
private recordAdapterSeen;
|
|
2075
|
+
/** Refresh-only for paths without an origin budget gate (install-scan, audit). */
|
|
2076
|
+
private touchAdapter;
|
|
2077
|
+
/**
|
|
2078
|
+
* Log a version sighting/change, capped per origin per boot. Both origin and
|
|
2079
|
+
* version are caller-controlled free text, so both are JSON-escaped — a
|
|
2080
|
+
* newline or control character must not be able to forge extra log lines
|
|
2081
|
+
* (the route's origin regex does not protect direct embedders).
|
|
2082
|
+
*/
|
|
2083
|
+
private logVersionEvent;
|
|
2034
2084
|
resolveApproval(ticketId: string, req: ResolveApprovalInput): ServiceResult;
|
|
2035
2085
|
sweep(): void;
|
|
2036
2086
|
/**
|
|
@@ -2338,6 +2388,14 @@ interface DashboardAppDeps {
|
|
|
2338
2388
|
readonly spendLimiter: SpendLimiter;
|
|
2339
2389
|
readonly evidenceStore: EvidenceStore;
|
|
2340
2390
|
readonly eventBus: DashboardEventBus;
|
|
2391
|
+
/**
|
|
2392
|
+
* Adapter liveness source for `GET /api/adapters` (issue #126) — a narrow
|
|
2393
|
+
* view of the SDK sideband's GovernanceService. Absent when the SDK
|
|
2394
|
+
* sideband is disabled; the endpoint then serves an empty list.
|
|
2395
|
+
*/
|
|
2396
|
+
readonly adapterLiveness?: {
|
|
2397
|
+
listAdapters(): AdapterLivenessEntry[];
|
|
2398
|
+
};
|
|
2341
2399
|
}
|
|
2342
2400
|
/** Options for the dashboard API. */
|
|
2343
2401
|
interface DashboardAppOptions {
|
|
@@ -2356,4 +2414,4 @@ interface DashboardAppOptions {
|
|
|
2356
2414
|
*/
|
|
2357
2415
|
declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
|
|
2358
2416
|
|
|
2359
|
-
export { type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type CompilePoliciesResult, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
|
|
2417
|
+
export { type AdapterLivenessEntry, type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type CompilePoliciesResult, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, EXPORT_MAX_RECORDS, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, LIST_MAX_PAGE_SIZE, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
|
package/dist/index.js
CHANGED
|
@@ -4873,6 +4873,7 @@ var MAX_PENDING_BYTES = 64 * 1024 * 1024;
|
|
|
4873
4873
|
var MAX_SENDER_KEYS = 5e4;
|
|
4874
4874
|
var MAX_EVIDENCE_ENTRIES = 16;
|
|
4875
4875
|
var MAX_EVIDENCE_BYTES = 64 * 1024;
|
|
4876
|
+
var MAX_VERSION_LOG_LINES_PER_ORIGIN = 5;
|
|
4876
4877
|
var SWEEP_INTERVAL_MS2 = 3e4;
|
|
4877
4878
|
var GovernanceService = class {
|
|
4878
4879
|
policy;
|
|
@@ -4890,6 +4891,13 @@ var GovernanceService = class {
|
|
|
4890
4891
|
maxSenderKeys;
|
|
4891
4892
|
/** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
|
|
4892
4893
|
senderKeys = /* @__PURE__ */ new Set();
|
|
4894
|
+
/**
|
|
4895
|
+
* Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
|
|
4896
|
+
* the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
|
|
4897
|
+
* other path updates existing entries and skips unknown origins, so the
|
|
4898
|
+
* registry shares the origin cap instead of adding a second growth vector.
|
|
4899
|
+
*/
|
|
4900
|
+
adapters = /* @__PURE__ */ new Map();
|
|
4893
4901
|
pending = /* @__PURE__ */ new Map();
|
|
4894
4902
|
tombstones = /* @__PURE__ */ new Map();
|
|
4895
4903
|
caches = /* @__PURE__ */ new Map();
|
|
@@ -4947,6 +4955,7 @@ var GovernanceService = class {
|
|
|
4947
4955
|
return { status: 503, body: { error: "evaluation_backlog_full" } };
|
|
4948
4956
|
}
|
|
4949
4957
|
const cache = this.cacheFor(req.origin);
|
|
4958
|
+
this.recordAdapterSeen(req.origin, req.adapter_version);
|
|
4950
4959
|
const toolName = req.tool.name;
|
|
4951
4960
|
const hasDefinition = definitionProvided(req.tool);
|
|
4952
4961
|
if (hasDefinition) {
|
|
@@ -5205,6 +5214,7 @@ var GovernanceService = class {
|
|
|
5205
5214
|
finalizedBy: "audit",
|
|
5206
5215
|
expiresAtMs: this.now() + this.ttlMs
|
|
5207
5216
|
});
|
|
5217
|
+
this.touchAdapter(entry.origin);
|
|
5208
5218
|
const body = { ok: true, audit_record_id: auditId };
|
|
5209
5219
|
if (evidenceOutcomes) body["evidence"] = evidenceOutcomes;
|
|
5210
5220
|
return { status: 201, body };
|
|
@@ -5270,6 +5280,7 @@ var GovernanceService = class {
|
|
|
5270
5280
|
if (reserved) {
|
|
5271
5281
|
return { status: 400, body: { error: "reserved_metadata_key", key: reserved } };
|
|
5272
5282
|
}
|
|
5283
|
+
this.touchAdapter(req.origin);
|
|
5273
5284
|
const evaluationId = randomUUID2();
|
|
5274
5285
|
const toolName = `install:${req.package.source ?? "pkg"}:${req.package.name}`;
|
|
5275
5286
|
const verdict = this.evaluateInstall(req);
|
|
@@ -5334,6 +5345,65 @@ var GovernanceService = class {
|
|
|
5334
5345
|
};
|
|
5335
5346
|
}
|
|
5336
5347
|
// -------------------------------------------------------------------------
|
|
5348
|
+
// Adapter liveness registry (issue #126)
|
|
5349
|
+
// -------------------------------------------------------------------------
|
|
5350
|
+
/** Wire-ready liveness entries, most recently seen first. */
|
|
5351
|
+
listAdapters() {
|
|
5352
|
+
return [...this.adapters.entries()].sort(([oa, a], [ob, b]) => b.lastSeenMs - a.lastSeenMs || oa.localeCompare(ob)).map(([origin, state]) => ({
|
|
5353
|
+
origin,
|
|
5354
|
+
adapter_version: state.adapterVersion,
|
|
5355
|
+
first_seen: new Date(state.firstSeenMs).toISOString(),
|
|
5356
|
+
last_seen: new Date(state.lastSeenMs).toISOString()
|
|
5357
|
+
}));
|
|
5358
|
+
}
|
|
5359
|
+
/** Insert-or-refresh on the /evaluate path (the only insert site). */
|
|
5360
|
+
recordAdapterSeen(origin, version) {
|
|
5361
|
+
const normalized = version && version.length <= 64 ? version : void 0;
|
|
5362
|
+
const now = this.now();
|
|
5363
|
+
const existing = this.adapters.get(origin);
|
|
5364
|
+
if (!existing) {
|
|
5365
|
+
const state = {
|
|
5366
|
+
adapterVersion: normalized ?? null,
|
|
5367
|
+
firstSeenMs: now,
|
|
5368
|
+
lastSeenMs: now,
|
|
5369
|
+
versionLogCount: 0
|
|
5370
|
+
};
|
|
5371
|
+
this.adapters.set(origin, state);
|
|
5372
|
+
if (normalized !== void 0) this.logVersionEvent(origin, state, null, normalized);
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, now);
|
|
5376
|
+
if (normalized !== void 0 && normalized !== existing.adapterVersion) {
|
|
5377
|
+
this.logVersionEvent(origin, existing, existing.adapterVersion, normalized);
|
|
5378
|
+
existing.adapterVersion = normalized;
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
/** Refresh-only for paths without an origin budget gate (install-scan, audit). */
|
|
5382
|
+
touchAdapter(origin) {
|
|
5383
|
+
const existing = this.adapters.get(origin);
|
|
5384
|
+
if (!existing) return;
|
|
5385
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, this.now());
|
|
5386
|
+
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Log a version sighting/change, capped per origin per boot. Both origin and
|
|
5389
|
+
* version are caller-controlled free text, so both are JSON-escaped — a
|
|
5390
|
+
* newline or control character must not be able to forge extra log lines
|
|
5391
|
+
* (the route's origin regex does not protect direct embedders).
|
|
5392
|
+
*/
|
|
5393
|
+
logVersionEvent(origin, state, from, to) {
|
|
5394
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) return;
|
|
5395
|
+
state.versionLogCount += 1;
|
|
5396
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) {
|
|
5397
|
+
console.error(
|
|
5398
|
+
`[helio] adapter origin ${JSON.stringify(origin)}: suppressing further version logs after ${String(MAX_VERSION_LOG_LINES_PER_ORIGIN)}`
|
|
5399
|
+
);
|
|
5400
|
+
return;
|
|
5401
|
+
}
|
|
5402
|
+
console.error(
|
|
5403
|
+
from === null ? `[helio] adapter origin ${JSON.stringify(origin)} reports version ${JSON.stringify(to)}` : `[helio] adapter origin ${JSON.stringify(origin)} version changed ${JSON.stringify(from)} -> ${JSON.stringify(to)}`
|
|
5404
|
+
);
|
|
5405
|
+
}
|
|
5406
|
+
// -------------------------------------------------------------------------
|
|
5337
5407
|
// POST /approval/:id/resolve
|
|
5338
5408
|
// -------------------------------------------------------------------------
|
|
5339
5409
|
resolveApproval(ticketId, req) {
|
|
@@ -5435,6 +5505,7 @@ var GovernanceService = class {
|
|
|
5435
5505
|
this.tombstones.clear();
|
|
5436
5506
|
this.caches.clear();
|
|
5437
5507
|
this.senderKeys.clear();
|
|
5508
|
+
this.adapters.clear();
|
|
5438
5509
|
this.pendingBytes = 0;
|
|
5439
5510
|
}
|
|
5440
5511
|
// -------------------------------------------------------------------------
|
|
@@ -5788,6 +5859,8 @@ function clampInt(value, fallback, min, max) {
|
|
|
5788
5859
|
|
|
5789
5860
|
// src/audit/store.ts
|
|
5790
5861
|
var DRIFT_EVENT_DECISIONS_SQL = "('tool_drift', 'tool_drift_reverted')";
|
|
5862
|
+
var EXPORT_MAX_RECORDS = 1e4;
|
|
5863
|
+
var LIST_MAX_PAGE_SIZE = 1e3;
|
|
5791
5864
|
var CREATE_TABLE_DDL = `
|
|
5792
5865
|
CREATE TABLE IF NOT EXISTS audit_records (
|
|
5793
5866
|
id TEXT PRIMARY KEY,
|
|
@@ -6091,15 +6164,28 @@ var AuditStore = class {
|
|
|
6091
6164
|
const row = this.db.prepare("SELECT * FROM audit_records WHERE id = ?").get(id);
|
|
6092
6165
|
return row ? deserializeRow(row) : void 0;
|
|
6093
6166
|
}
|
|
6094
|
-
/** Query records with filters and pagination. */
|
|
6167
|
+
/** Query records with filters and pagination. Capped at 1,000 per page. */
|
|
6095
6168
|
list(filters = {}, pagination = {}) {
|
|
6096
|
-
const
|
|
6097
|
-
const limit = clamp(pagination.limit ?? 50, 1, 1e3);
|
|
6169
|
+
const limit = clamp(pagination.limit ?? 50, 1, LIST_MAX_PAGE_SIZE);
|
|
6098
6170
|
const offset = Math.max(pagination.offset ?? 0, 0);
|
|
6099
6171
|
const order = pagination.order === "asc" ? "ASC" : "DESC";
|
|
6172
|
+
return this.query(filters, limit, offset, order);
|
|
6173
|
+
}
|
|
6174
|
+
/**
|
|
6175
|
+
* Query records for bulk export. Unlike `list()`, which enforces the
|
|
6176
|
+
* dashboard's 1,000-row page cap, this path allows up to
|
|
6177
|
+
* {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
|
|
6178
|
+
* (ascending `created_at`), so a capped export keeps the earliest records.
|
|
6179
|
+
*/
|
|
6180
|
+
listForExport(filters = {}, limit) {
|
|
6181
|
+
const clamped = clamp(limit ?? EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS);
|
|
6182
|
+
return this.query(filters, clamped, 0, "ASC");
|
|
6183
|
+
}
|
|
6184
|
+
query(filters, limit, offset, order) {
|
|
6185
|
+
const { clause, params } = buildWhereClause(filters);
|
|
6100
6186
|
const { total } = this.db.prepare(`SELECT COUNT(*) as total FROM audit_records ${clause}`).get(...params);
|
|
6101
6187
|
const rows = this.db.prepare(
|
|
6102
|
-
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order} LIMIT ? OFFSET ?`
|
|
6188
|
+
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order}, rowid ${order} LIMIT ? OFFSET ?`
|
|
6103
6189
|
).all(...params, limit, offset);
|
|
6104
6190
|
return {
|
|
6105
6191
|
records: rows.map(deserializeRow),
|
|
@@ -7302,7 +7388,10 @@ var CSV_HEADERS = [
|
|
|
7302
7388
|
"dry_run",
|
|
7303
7389
|
"created_at",
|
|
7304
7390
|
"environment",
|
|
7305
|
-
"matched_rule_index"
|
|
7391
|
+
"matched_rule_index",
|
|
7392
|
+
"record_kind",
|
|
7393
|
+
"origin",
|
|
7394
|
+
"metadata"
|
|
7306
7395
|
];
|
|
7307
7396
|
var FORMULA_PREFIXES = /^[=+\-@\t\r]/;
|
|
7308
7397
|
function csvEscape(value) {
|
|
@@ -7448,7 +7537,7 @@ var feedQuerySchema = z8.object({
|
|
|
7448
7537
|
});
|
|
7449
7538
|
var auditExportQuerySchema = z8.object({
|
|
7450
7539
|
format: z8.preprocess((value) => value === "csv" ? "csv" : "json", z8.enum(["json", "csv"])),
|
|
7451
|
-
limit: clampedQueryInt(
|
|
7540
|
+
limit: clampedQueryInt(EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS),
|
|
7452
7541
|
tool: optionalQueryString,
|
|
7453
7542
|
decision: optionalQueryString,
|
|
7454
7543
|
reason: optionalQueryString,
|
|
@@ -7466,7 +7555,7 @@ var auditExportQuerySchema = z8.object({
|
|
|
7466
7555
|
sender_id: optionalQueryString
|
|
7467
7556
|
});
|
|
7468
7557
|
var auditQuerySchema = z8.object({
|
|
7469
|
-
limit: clampedQueryInt(50, 1,
|
|
7558
|
+
limit: clampedQueryInt(50, 1, LIST_MAX_PAGE_SIZE),
|
|
7470
7559
|
offset: clampedQueryInt(0, 0, Number.MAX_SAFE_INTEGER),
|
|
7471
7560
|
tool: optionalQueryString,
|
|
7472
7561
|
decision: optionalQueryString,
|
|
@@ -7556,7 +7645,8 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7556
7645
|
rateLimiter,
|
|
7557
7646
|
spendLimiter,
|
|
7558
7647
|
evidenceStore,
|
|
7559
|
-
eventBus
|
|
7648
|
+
eventBus,
|
|
7649
|
+
adapterLiveness
|
|
7560
7650
|
} = deps;
|
|
7561
7651
|
const apiSecret = options?.apiSecret;
|
|
7562
7652
|
const sessionStore = apiSecret ? new DashboardSessionStore({ secret: apiSecret, ttlMs: SESSION_TTL_MS }) : void 0;
|
|
@@ -7711,7 +7801,7 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7711
7801
|
channel_id: query.channel_id,
|
|
7712
7802
|
sender_id: query.sender_id
|
|
7713
7803
|
};
|
|
7714
|
-
const result = auditStore.
|
|
7804
|
+
const result = auditStore.listForExport(filters, limit);
|
|
7715
7805
|
if (format === "csv") {
|
|
7716
7806
|
const csv = recordsToCsv(result.records);
|
|
7717
7807
|
return new Response(csv, {
|
|
@@ -7775,6 +7865,9 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7775
7865
|
spend_limits: spendLimiter.listKeyStates()
|
|
7776
7866
|
});
|
|
7777
7867
|
});
|
|
7868
|
+
app.get("/api/adapters", (c) => {
|
|
7869
|
+
return c.json({ adapters: adapterLiveness?.listAdapters() ?? [] });
|
|
7870
|
+
});
|
|
7778
7871
|
app.get("/api/analytics", (c) => {
|
|
7779
7872
|
const query = analyticsQuerySchema.parse(c.req.query());
|
|
7780
7873
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -7945,10 +8038,12 @@ export {
|
|
|
7945
8038
|
AuditWriter,
|
|
7946
8039
|
ConfigError,
|
|
7947
8040
|
DashboardEventBus,
|
|
8041
|
+
EXPORT_MAX_RECORDS,
|
|
7948
8042
|
EvidenceStore,
|
|
7949
8043
|
GovernanceConfigError,
|
|
7950
8044
|
GovernanceService,
|
|
7951
8045
|
GovernedForwarder,
|
|
8046
|
+
LIST_MAX_PAGE_SIZE,
|
|
7952
8047
|
PolicyParseError,
|
|
7953
8048
|
QueueChannel,
|
|
7954
8049
|
RateLimiter,
|