@akasecurity/ai-tc-claude-code 0.9.3 → 0.9.5
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/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/commands/setup.md +123 -35
- package/hooks/hooks.json +11 -0
- package/package.json +7 -6
- package/scripts/apply-suppressions.js +2178 -593
- package/scripts/backfill.js +3815 -429
- package/scripts/filescan.js +2379 -336
- package/scripts/firstrun.js +1748 -181
- package/scripts/intro.js +520 -48
- package/scripts/message-display.js +30054 -0
- package/scripts/onboard.js +2007 -208
- package/scripts/post-tool-use.js +3721 -388
- package/scripts/pre-tool-use.js +3900 -414
- package/scripts/query.js +1752 -181
- package/scripts/reconcile.js +3321 -372
- package/scripts/remediate.js +3925 -582
- package/scripts/scan-worker.js +18006 -0
- package/scripts/session-start.js +1879 -273
- package/scripts/start-light.js +540 -68
- package/scripts/statusline.js +1747 -180
- package/scripts/stop.js +504 -75
- package/scripts/user-prompt-submit.js +3695 -408
package/scripts/reconcile.js
CHANGED
|
@@ -492,13 +492,12 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// ../../packages/plugin-sdk/src/config.ts
|
|
495
|
-
import { existsSync as
|
|
496
|
-
import { join as
|
|
495
|
+
import { existsSync as existsSync5 } from "fs";
|
|
496
|
+
import { join as join8 } from "path";
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/database.ts
|
|
499
|
-
import { randomUUID as
|
|
500
|
-
import {
|
|
501
|
-
import { join, sep } from "path";
|
|
499
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
500
|
+
import { join as join2, sep } from "path";
|
|
502
501
|
import { DatabaseSync } from "node:sqlite";
|
|
503
502
|
|
|
504
503
|
// ../../packages/schema/src/drizzle/sqlite-ddl.ts
|
|
@@ -562,6 +561,30 @@ var SQLITE_MIGRATIONS = [
|
|
|
562
561
|
{
|
|
563
562
|
tag: "0014_drop_legacy_events_findings",
|
|
564
563
|
sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
tag: "0015_busy_vengeance",
|
|
567
|
+
sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
tag: "0016_breezy_zodiak",
|
|
571
|
+
sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
tag: "0017_rainy_kat_farrell",
|
|
575
|
+
sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
tag: "0018_serious_tana_nile",
|
|
579
|
+
sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
tag: "0019_audit_started_at_index",
|
|
583
|
+
sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
tag: "0020_secret_vault_pagination_indexes",
|
|
587
|
+
sql: "CREATE INDEX `idx_secret_vault_last_seen` ON `secret_vault` (`last_seen`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_reuse` ON `secret_vault` (`occurrence_count`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_at` ON `secret_vault_deref` (`at`,`id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_signal` ON `secret_vault_deref` (`at`,`id`) WHERE `reason` NOT IN ('display', 'view-render');"
|
|
565
588
|
}
|
|
566
589
|
];
|
|
567
590
|
|
|
@@ -15299,7 +15322,17 @@ var Finding = external_exports.object({
|
|
|
15299
15322
|
}).meta({ id: "Finding" });
|
|
15300
15323
|
var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
|
|
15301
15324
|
var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
|
|
15302
|
-
var FindingProvider = external_exports.enum([
|
|
15325
|
+
var FindingProvider = external_exports.enum([
|
|
15326
|
+
"claudecode",
|
|
15327
|
+
"claudedesktop",
|
|
15328
|
+
"cursor",
|
|
15329
|
+
"copilot",
|
|
15330
|
+
"chatgpt",
|
|
15331
|
+
"claudeai",
|
|
15332
|
+
"codex",
|
|
15333
|
+
"antigravity",
|
|
15334
|
+
"api"
|
|
15335
|
+
]).meta({ id: "FindingProvider" });
|
|
15303
15336
|
var FindingCategory = external_exports.enum([
|
|
15304
15337
|
"secret",
|
|
15305
15338
|
"pii",
|
|
@@ -15353,7 +15386,16 @@ var FindingInstance = external_exports.object({
|
|
|
15353
15386
|
confidence: external_exports.number().min(0).max(1),
|
|
15354
15387
|
// Lifecycle status (see FindingStatus). Optional so legacy callers/rows
|
|
15355
15388
|
// that predate the resolution feature stay valid.
|
|
15356
|
-
status: FindingStatus.optional()
|
|
15389
|
+
status: FindingStatus.optional(),
|
|
15390
|
+
// The audit event this finding was captured from. Optional so callers that
|
|
15391
|
+
// do not project it stay valid. An at-rest finding is content-addressed by
|
|
15392
|
+
// finding_key and its row is upserted on re-detection, so this names the
|
|
15393
|
+
// MOST RECENT detection event, not the first.
|
|
15394
|
+
eventId: external_exports.string().optional(),
|
|
15395
|
+
// The session that event belongs to, when it has one — the seam a
|
|
15396
|
+
// per-instance "view session" link needs. Absent for events captured
|
|
15397
|
+
// outside a session.
|
|
15398
|
+
sessionId: external_exports.string().optional()
|
|
15357
15399
|
}).meta({ id: "FindingInstance" });
|
|
15358
15400
|
var FindingGroup = external_exports.object({
|
|
15359
15401
|
id: external_exports.string(),
|
|
@@ -15397,7 +15439,11 @@ var FindingFacets = external_exports.object({
|
|
|
15397
15439
|
// for every instance, so every group lands in a bucket; a status-less
|
|
15398
15440
|
// group (possible only for callers whose rows carry no statuses) is
|
|
15399
15441
|
// counted under no value.
|
|
15400
|
-
status: external_exports.array(FindingFacetItem)
|
|
15442
|
+
status: external_exports.array(FindingFacetItem),
|
|
15443
|
+
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
15444
|
+
// reads, which can filter by it; the grouped read omits the dimension
|
|
15445
|
+
// because a group spans tools.
|
|
15446
|
+
tool: external_exports.array(FindingFacetItem).optional()
|
|
15401
15447
|
}).meta({ id: "FindingFacets" });
|
|
15402
15448
|
var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
|
|
15403
15449
|
var ListGroupedFindingsQuery = external_exports.object({
|
|
@@ -15415,6 +15461,16 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
15415
15461
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
15416
15462
|
// session → findings drilldown). Findings without a session never match.
|
|
15417
15463
|
sessionId: external_exports.string().optional(),
|
|
15464
|
+
// Inclusive lower bound on the parent event's timestamp, so a caller arriving
|
|
15465
|
+
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
15466
|
+
// means all time — this list has no default window.
|
|
15467
|
+
from: external_exports.iso.datetime().optional(),
|
|
15468
|
+
// A group or instance id that must appear in the page even when the cursor
|
|
15469
|
+
// has already advanced past its sort position. This is what keeps the
|
|
15470
|
+
// Findings page's one-shot ?finding= deep link resolving once the list
|
|
15471
|
+
// paginates: the target group is appended out of sort order rather than
|
|
15472
|
+
// scanning forward for it. Never affects totals, facets or the cursor.
|
|
15473
|
+
includeId: external_exports.string().optional(),
|
|
15418
15474
|
groupBy: external_exports.literal("type").optional(),
|
|
15419
15475
|
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
15420
15476
|
cursor: external_exports.string().optional()
|
|
@@ -15459,15 +15515,110 @@ var FindingInstanceDetail = FindingInstance.extend({
|
|
|
15459
15515
|
detection: FindingDetectionRef,
|
|
15460
15516
|
policy: FindingPolicyRef
|
|
15461
15517
|
}).meta({ id: "FindingInstanceDetail" });
|
|
15518
|
+
var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
15519
|
+
var ListFindingInstancesQuery = external_exports.object({
|
|
15520
|
+
severity: external_exports.array(Severity).optional(),
|
|
15521
|
+
// Rule ids, the same vocabulary the grouped list's `subtype` carries.
|
|
15522
|
+
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15523
|
+
provider: external_exports.array(FindingProvider).optional(),
|
|
15524
|
+
action: external_exports.array(FindingAction).optional(),
|
|
15525
|
+
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
15526
|
+
// the grouped query's group-level fold.
|
|
15527
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15528
|
+
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
15529
|
+
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
15530
|
+
tool: external_exports.array(external_exports.string()).optional(),
|
|
15531
|
+
// Exact repository / file-path matches, for the drill-down out of the
|
|
15532
|
+
// locations view. A row whose event carries no repo/file matches neither.
|
|
15533
|
+
repo: external_exports.string().optional(),
|
|
15534
|
+
file: external_exports.string().optional(),
|
|
15535
|
+
q: external_exports.string().optional(),
|
|
15536
|
+
sessionId: external_exports.string().optional(),
|
|
15537
|
+
from: external_exports.iso.datetime().optional(),
|
|
15538
|
+
limit: external_exports.coerce.number().int().min(1).max(200).optional(),
|
|
15539
|
+
cursor: external_exports.string().optional()
|
|
15540
|
+
});
|
|
15541
|
+
var ListFindingInstancesResponse = external_exports.object({
|
|
15542
|
+
// Instances matching the filters across the whole scope, not just this
|
|
15543
|
+
// page — cursor-independent, like the grouped list's totals.
|
|
15544
|
+
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
15545
|
+
// Counts in INSTANCES here, where the grouped response counts groups. Each
|
|
15546
|
+
// dimension still excludes its own filter.
|
|
15547
|
+
facets: FindingFacets,
|
|
15548
|
+
items: external_exports.array(FindingInstanceDetail),
|
|
15549
|
+
nextCursor: external_exports.string().nullable()
|
|
15550
|
+
}).meta({ id: "ListFindingInstancesResponse" });
|
|
15551
|
+
var FindingLocationFile = external_exports.object({
|
|
15552
|
+
// Empty when the instances carried no file path (a prompt or a tool call
|
|
15553
|
+
// with no file attribution).
|
|
15554
|
+
file: external_exports.string(),
|
|
15555
|
+
instanceCount: external_exports.number().int().nonnegative(),
|
|
15556
|
+
maxSeverity: Severity,
|
|
15557
|
+
latestDetectedAt: external_exports.iso.datetime(),
|
|
15558
|
+
// Folded from the instances' derived statuses with the same
|
|
15559
|
+
// open-dominates precedence a group uses.
|
|
15560
|
+
status: FindingStatus.optional(),
|
|
15561
|
+
// Distinct rules seen at this location, capped — the row shows them as
|
|
15562
|
+
// chips, and the count is what conveys scale.
|
|
15563
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
15564
|
+
}).meta({ id: "FindingLocationFile" });
|
|
15565
|
+
var FindingLocationRepo = external_exports.object({
|
|
15566
|
+
/** Empty when the instances carried no repo attribute. */
|
|
15567
|
+
repo: external_exports.string(),
|
|
15568
|
+
instanceCount: external_exports.number().int().nonnegative(),
|
|
15569
|
+
maxSeverity: Severity,
|
|
15570
|
+
latestDetectedAt: external_exports.iso.datetime(),
|
|
15571
|
+
status: FindingStatus.optional(),
|
|
15572
|
+
files: external_exports.array(FindingLocationFile)
|
|
15573
|
+
}).meta({ id: "FindingLocationRepo" });
|
|
15574
|
+
var ListFindingLocationsQuery = external_exports.object({
|
|
15575
|
+
severity: external_exports.array(Severity).optional(),
|
|
15576
|
+
subtype: external_exports.array(external_exports.string()).optional(),
|
|
15577
|
+
provider: external_exports.array(FindingProvider).optional(),
|
|
15578
|
+
action: external_exports.array(FindingAction).optional(),
|
|
15579
|
+
// Per-instance, as in ListFindingInstancesQuery: a location keeps the
|
|
15580
|
+
// instances that match, and folds its status from those.
|
|
15581
|
+
status: external_exports.array(FindingStatus).optional(),
|
|
15582
|
+
tool: external_exports.array(external_exports.string()).optional(),
|
|
15583
|
+
q: external_exports.string().optional(),
|
|
15584
|
+
sessionId: external_exports.string().optional(),
|
|
15585
|
+
from: external_exports.iso.datetime().optional(),
|
|
15586
|
+
limit: external_exports.coerce.number().int().min(1).max(500).optional()
|
|
15587
|
+
});
|
|
15588
|
+
var ListFindingLocationsResponse = external_exports.object({
|
|
15589
|
+
totals: external_exports.object({
|
|
15590
|
+
findings: external_exports.number().int().nonnegative(),
|
|
15591
|
+
repos: external_exports.number().int().nonnegative(),
|
|
15592
|
+
files: external_exports.number().int().nonnegative()
|
|
15593
|
+
}),
|
|
15594
|
+
/** Sorted by max severity, then most recent. */
|
|
15595
|
+
items: external_exports.array(FindingLocationRepo),
|
|
15596
|
+
/** Whether `limit` truncated the repo list. */
|
|
15597
|
+
hasMore: external_exports.boolean()
|
|
15598
|
+
}).meta({ id: "ListFindingLocationsResponse" });
|
|
15462
15599
|
|
|
15463
15600
|
// ../../packages/schema/src/zod/harness-map.ts
|
|
15464
|
-
var Harness = external_exports.enum([
|
|
15601
|
+
var Harness = external_exports.enum([
|
|
15602
|
+
"claudecode",
|
|
15603
|
+
"cursor",
|
|
15604
|
+
"copilot",
|
|
15605
|
+
"codex",
|
|
15606
|
+
"antigravity",
|
|
15607
|
+
"windsurf",
|
|
15608
|
+
"claudedesktop",
|
|
15609
|
+
"chatgpt",
|
|
15610
|
+
"claudeai",
|
|
15611
|
+
"api"
|
|
15612
|
+
]).meta({ id: "Harness" });
|
|
15465
15613
|
var TOOL_TO_HARNESS = {
|
|
15466
15614
|
"claude-code": "claudecode",
|
|
15467
15615
|
"claude-desktop": "claudedesktop",
|
|
15468
15616
|
"github-copilot": "copilot",
|
|
15469
15617
|
cursor: "cursor",
|
|
15470
|
-
chatgpt: "chatgpt"
|
|
15618
|
+
chatgpt: "chatgpt",
|
|
15619
|
+
codex: "codex",
|
|
15620
|
+
antigravity: "antigravity",
|
|
15621
|
+
"claude-ai": "claudeai"
|
|
15471
15622
|
};
|
|
15472
15623
|
function harnessFromTool(tool) {
|
|
15473
15624
|
return TOOL_TO_HARNESS[tool] ?? tool;
|
|
@@ -15928,7 +16079,18 @@ var ActivityOverviewResponse = external_exports.object({
|
|
|
15928
16079
|
// ../../packages/schema/src/zod/event.ts
|
|
15929
16080
|
var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
|
|
15930
16081
|
var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
|
|
15931
|
-
var SourceTool = external_exports.enum([
|
|
16082
|
+
var SourceTool = external_exports.enum([
|
|
16083
|
+
"claude-code",
|
|
16084
|
+
"claude-desktop",
|
|
16085
|
+
"cursor",
|
|
16086
|
+
"chatgpt",
|
|
16087
|
+
"claude-ai",
|
|
16088
|
+
"github-copilot",
|
|
16089
|
+
"codex",
|
|
16090
|
+
"antigravity",
|
|
16091
|
+
"cli",
|
|
16092
|
+
"unknown"
|
|
16093
|
+
]).meta({ id: "SourceTool" });
|
|
15932
16094
|
var EventMetadata = external_exports.object({
|
|
15933
16095
|
sessionId: external_exports.string().optional(),
|
|
15934
16096
|
repo: external_exports.string().optional(),
|
|
@@ -15999,7 +16161,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
|
|
|
15999
16161
|
var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
|
|
16000
16162
|
var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
|
|
16001
16163
|
var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
|
|
16002
|
-
var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
|
|
16164
|
+
var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
|
|
16003
16165
|
var AccessCounts = external_exports.object({
|
|
16004
16166
|
open: external_exports.number().int().nonnegative(),
|
|
16005
16167
|
approved: external_exports.number().int().nonnegative(),
|
|
@@ -16221,6 +16383,7 @@ var ExceptionConditions = external_exports.object({
|
|
|
16221
16383
|
sourceTool: external_exports.string().optional(),
|
|
16222
16384
|
provider: external_exports.string().optional()
|
|
16223
16385
|
}).strict();
|
|
16386
|
+
var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
|
|
16224
16387
|
var DetectionException = external_exports.object({
|
|
16225
16388
|
id: external_exports.guid(),
|
|
16226
16389
|
ruleId: external_exports.string(),
|
|
@@ -16237,6 +16400,7 @@ var DetectionException = external_exports.object({
|
|
|
16237
16400
|
keyVersion: external_exports.number().int().positive(),
|
|
16238
16401
|
// maskMatch() preview of the approved value — never the raw value.
|
|
16239
16402
|
maskedValue: external_exports.string(),
|
|
16403
|
+
capability: ExceptionCapability.default("suppress"),
|
|
16240
16404
|
scope: ExceptionScope,
|
|
16241
16405
|
expiresAt: external_exports.iso.datetime().nullable(),
|
|
16242
16406
|
maxUses: external_exports.number().int().positive().nullable(),
|
|
@@ -16260,11 +16424,13 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16260
16424
|
ruleId: true,
|
|
16261
16425
|
valueFingerprint: true,
|
|
16262
16426
|
keyVersion: true,
|
|
16427
|
+
capability: true,
|
|
16263
16428
|
expiresAt: true,
|
|
16264
16429
|
maxUses: true,
|
|
16265
16430
|
useCount: true,
|
|
16266
16431
|
conditions: true
|
|
16267
16432
|
});
|
|
16433
|
+
var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
|
|
16268
16434
|
|
|
16269
16435
|
// ../../packages/schema/src/zod/rule.ts
|
|
16270
16436
|
var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
|
|
@@ -17075,6 +17241,35 @@ var EgressWriteSummary = external_exports.object({
|
|
|
17075
17241
|
droppedFiles: external_exports.array(external_exports.string()).default([])
|
|
17076
17242
|
}).meta({ id: "EgressWriteSummary" });
|
|
17077
17243
|
|
|
17244
|
+
// ../../packages/schema/src/zod/exception-action.ts
|
|
17245
|
+
var confirmation = external_exports.string().optional();
|
|
17246
|
+
var ApproveBlockedInput = external_exports.object({
|
|
17247
|
+
reference: external_exports.string(),
|
|
17248
|
+
scope: external_exports.string(),
|
|
17249
|
+
reason: external_exports.string(),
|
|
17250
|
+
confirmation
|
|
17251
|
+
});
|
|
17252
|
+
var AddExceptionInput = external_exports.object({
|
|
17253
|
+
ruleId: external_exports.string(),
|
|
17254
|
+
value: external_exports.string(),
|
|
17255
|
+
scope: external_exports.string(),
|
|
17256
|
+
reason: external_exports.string(),
|
|
17257
|
+
confirmation
|
|
17258
|
+
});
|
|
17259
|
+
var GrantRevealInput = external_exports.object({
|
|
17260
|
+
pointer: external_exports.string(),
|
|
17261
|
+
scope: external_exports.string(),
|
|
17262
|
+
justification: external_exports.string(),
|
|
17263
|
+
confirmation
|
|
17264
|
+
});
|
|
17265
|
+
var RevokeExceptionInput = external_exports.object({
|
|
17266
|
+
id: external_exports.string(),
|
|
17267
|
+
reason: external_exports.string()
|
|
17268
|
+
});
|
|
17269
|
+
var RotateKeyInput = external_exports.object({
|
|
17270
|
+
confirmation: external_exports.string()
|
|
17271
|
+
});
|
|
17272
|
+
|
|
17078
17273
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
17079
17274
|
function toApiAction(dbVal) {
|
|
17080
17275
|
const map2 = {
|
|
@@ -17130,6 +17325,8 @@ function buildFindingGroups(rows, opts = {}) {
|
|
|
17130
17325
|
repo: r.repo,
|
|
17131
17326
|
file: r.file,
|
|
17132
17327
|
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
17328
|
+
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
17329
|
+
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
17133
17330
|
action: toApiAction(effectiveDbAction),
|
|
17134
17331
|
detectedAt: r.occurredAt,
|
|
17135
17332
|
confidence: r.confidence,
|
|
@@ -17261,14 +17458,17 @@ function applyFindingFilters(groups, opts) {
|
|
|
17261
17458
|
}
|
|
17262
17459
|
var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
17263
17460
|
var SEVERITY_RANK = SEVERITY_ORDER;
|
|
17461
|
+
function compareFindingGroupOrder(a, b) {
|
|
17462
|
+
const rankA = SEVERITY_RANK[a.severity] ?? -1;
|
|
17463
|
+
const rankB = SEVERITY_RANK[b.severity] ?? -1;
|
|
17464
|
+
const severityDiff = rankA - rankB;
|
|
17465
|
+
if (severityDiff !== 0) return severityDiff;
|
|
17466
|
+
const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
|
|
17467
|
+
if (recencyDiff !== 0) return recencyDiff;
|
|
17468
|
+
return a.id.localeCompare(b.id);
|
|
17469
|
+
}
|
|
17264
17470
|
function sortFindingGroups(groups) {
|
|
17265
|
-
return [...groups].sort(
|
|
17266
|
-
const rankA = SEVERITY_RANK[a.severity] ?? -1;
|
|
17267
|
-
const rankB = SEVERITY_RANK[b.severity] ?? -1;
|
|
17268
|
-
const severityDiff = rankA - rankB;
|
|
17269
|
-
if (severityDiff !== 0) return severityDiff;
|
|
17270
|
-
return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
|
|
17271
|
-
});
|
|
17471
|
+
return [...groups].sort(compareFindingGroupOrder);
|
|
17272
17472
|
}
|
|
17273
17473
|
function computeFindingFacets(allGroups, opts) {
|
|
17274
17474
|
const forSeverity = applyFindingFilters(allGroups, {
|
|
@@ -17324,15 +17524,158 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
17324
17524
|
for (const g of forStatus) {
|
|
17325
17525
|
if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
|
|
17326
17526
|
}
|
|
17327
|
-
const
|
|
17527
|
+
const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
|
|
17528
|
+
return {
|
|
17529
|
+
severity: toItems2(severityMap),
|
|
17530
|
+
provider: toItems2(providerMap),
|
|
17531
|
+
action: toItems2(actionMap),
|
|
17532
|
+
subtype: toItems2(subtypeMap),
|
|
17533
|
+
status: toItems2(statusMap)
|
|
17534
|
+
};
|
|
17535
|
+
}
|
|
17536
|
+
|
|
17537
|
+
// ../../packages/schema/src/zod/findings-flat-build.ts
|
|
17538
|
+
function rowHaystack(row) {
|
|
17539
|
+
return [
|
|
17540
|
+
row.ruleId,
|
|
17541
|
+
row.category,
|
|
17542
|
+
row.maskedMatch,
|
|
17543
|
+
row.repo,
|
|
17544
|
+
row.file,
|
|
17545
|
+
row.toolName ? `via ${row.toolName}` : "",
|
|
17546
|
+
row.id
|
|
17547
|
+
].join(" ").toLowerCase();
|
|
17548
|
+
}
|
|
17549
|
+
function matchesDimension(row, opts, dimension) {
|
|
17550
|
+
switch (dimension) {
|
|
17551
|
+
case "severity":
|
|
17552
|
+
return !opts.severity?.length || opts.severity.includes(row.severity);
|
|
17553
|
+
case "subtype":
|
|
17554
|
+
return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
|
|
17555
|
+
case "providers":
|
|
17556
|
+
return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
|
|
17557
|
+
case "actions":
|
|
17558
|
+
return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
|
|
17559
|
+
case "statuses":
|
|
17560
|
+
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
17561
|
+
case "tools":
|
|
17562
|
+
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
17563
|
+
case "repo":
|
|
17564
|
+
return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
|
|
17565
|
+
case "file":
|
|
17566
|
+
return opts.file === void 0 || opts.file === "" || row.file === opts.file;
|
|
17567
|
+
case "q":
|
|
17568
|
+
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
17569
|
+
}
|
|
17570
|
+
}
|
|
17571
|
+
var DIMENSIONS = [
|
|
17572
|
+
"severity",
|
|
17573
|
+
"subtype",
|
|
17574
|
+
"providers",
|
|
17575
|
+
"actions",
|
|
17576
|
+
"statuses",
|
|
17577
|
+
"tools",
|
|
17578
|
+
"repo",
|
|
17579
|
+
"file",
|
|
17580
|
+
"q"
|
|
17581
|
+
];
|
|
17582
|
+
function matchesInstanceFilters(row, opts, except) {
|
|
17583
|
+
for (const dimension of DIMENSIONS) {
|
|
17584
|
+
if (dimension === except) continue;
|
|
17585
|
+
if (!matchesDimension(row, opts, dimension)) return false;
|
|
17586
|
+
}
|
|
17587
|
+
return true;
|
|
17588
|
+
}
|
|
17589
|
+
function toItems(counts) {
|
|
17590
|
+
return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
17591
|
+
}
|
|
17592
|
+
function bump(counts, value) {
|
|
17593
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
17594
|
+
}
|
|
17595
|
+
function createInstanceFacetAccumulator(opts) {
|
|
17596
|
+
const severity = /* @__PURE__ */ new Map();
|
|
17597
|
+
const subtype = /* @__PURE__ */ new Map();
|
|
17598
|
+
const provider = /* @__PURE__ */ new Map();
|
|
17599
|
+
const action = /* @__PURE__ */ new Map();
|
|
17600
|
+
const status = /* @__PURE__ */ new Map();
|
|
17601
|
+
const tool = /* @__PURE__ */ new Map();
|
|
17602
|
+
return {
|
|
17603
|
+
add(row) {
|
|
17604
|
+
if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
|
|
17605
|
+
if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
|
|
17606
|
+
if (matchesInstanceFilters(row, opts, "providers")) {
|
|
17607
|
+
bump(provider, toApiProvider(row.sourceTool));
|
|
17608
|
+
}
|
|
17609
|
+
if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
|
|
17610
|
+
if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
|
|
17611
|
+
bump(status, row.status);
|
|
17612
|
+
}
|
|
17613
|
+
if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
|
|
17614
|
+
bump(tool, row.toolName);
|
|
17615
|
+
}
|
|
17616
|
+
},
|
|
17617
|
+
facets: () => ({
|
|
17618
|
+
severity: toItems(severity),
|
|
17619
|
+
subtype: toItems(subtype),
|
|
17620
|
+
provider: toItems(provider),
|
|
17621
|
+
action: toItems(action),
|
|
17622
|
+
status: toItems(status),
|
|
17623
|
+
tool: toItems(tool)
|
|
17624
|
+
})
|
|
17625
|
+
};
|
|
17626
|
+
}
|
|
17627
|
+
function toInstanceDetail(row) {
|
|
17628
|
+
const category = toApiCategory(row.category);
|
|
17328
17629
|
return {
|
|
17329
|
-
|
|
17330
|
-
provider:
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17630
|
+
id: row.id,
|
|
17631
|
+
provider: toApiProvider(row.sourceTool),
|
|
17632
|
+
repo: row.repo,
|
|
17633
|
+
file: row.file,
|
|
17634
|
+
...row.toolName === void 0 ? {} : { toolName: row.toolName },
|
|
17635
|
+
eventId: row.eventId,
|
|
17636
|
+
...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
|
|
17637
|
+
action: toApiAction(row.actionTaken),
|
|
17638
|
+
detectedAt: row.occurredAt,
|
|
17639
|
+
confidence: row.confidence,
|
|
17640
|
+
...row.status === void 0 ? {} : { status: row.status },
|
|
17641
|
+
groupId: row.ruleId,
|
|
17642
|
+
category,
|
|
17643
|
+
subtype: row.ruleId,
|
|
17644
|
+
severity: row.severity,
|
|
17645
|
+
match: { maskedValue: row.maskedMatch, contextPrefix: "" },
|
|
17646
|
+
detection: { id: row.ruleId, name: null },
|
|
17647
|
+
policy: { id: `category:${category}`, name: category }
|
|
17648
|
+
};
|
|
17649
|
+
}
|
|
17650
|
+
var SEVERITY_ORDER2 = {
|
|
17651
|
+
critical: 0,
|
|
17652
|
+
high: 1,
|
|
17653
|
+
medium: 2,
|
|
17654
|
+
low: 3
|
|
17655
|
+
};
|
|
17656
|
+
function newLocationAccumulator() {
|
|
17657
|
+
return {
|
|
17658
|
+
instanceCount: 0,
|
|
17659
|
+
// Sorts after every known severity, so the first row always wins the
|
|
17660
|
+
// comparison below rather than an unknown value pinning the location.
|
|
17661
|
+
maxSeverityRank: Number.MAX_SAFE_INTEGER,
|
|
17662
|
+
maxSeverity: "low",
|
|
17663
|
+
latestDetectedAt: "",
|
|
17664
|
+
statuses: [],
|
|
17665
|
+
ruleIds: /* @__PURE__ */ new Set()
|
|
17334
17666
|
};
|
|
17335
17667
|
}
|
|
17668
|
+
function addToLocation(acc, row) {
|
|
17669
|
+
acc.instanceCount += 1;
|
|
17670
|
+
const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
|
|
17671
|
+
if (rank < acc.maxSeverityRank) {
|
|
17672
|
+
acc.maxSeverityRank = rank;
|
|
17673
|
+
acc.maxSeverity = row.severity;
|
|
17674
|
+
}
|
|
17675
|
+
if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
|
|
17676
|
+
acc.statuses.push(row.status);
|
|
17677
|
+
acc.ruleIds.add(row.ruleId);
|
|
17678
|
+
}
|
|
17336
17679
|
|
|
17337
17680
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
17338
17681
|
var InstalledPack = external_exports.object({
|
|
@@ -17364,8 +17707,172 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17364
17707
|
message: "At least one field must be provided"
|
|
17365
17708
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17366
17709
|
|
|
17710
|
+
// ../../packages/schema/src/zod/vault.ts
|
|
17711
|
+
var POINTER_FORMAT_VERSION = 2;
|
|
17712
|
+
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
17713
|
+
var POINTER_TOKEN_PATTERN = new RegExp(
|
|
17714
|
+
`\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
|
|
17715
|
+
);
|
|
17716
|
+
var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
|
|
17717
|
+
function pointerTokenScanner() {
|
|
17718
|
+
return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
|
|
17719
|
+
}
|
|
17720
|
+
var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
|
|
17721
|
+
var ParsedPointer = external_exports.object({
|
|
17722
|
+
category: DetectionCategory,
|
|
17723
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17724
|
+
pointerId: external_exports.string(),
|
|
17725
|
+
tag: external_exports.string()
|
|
17726
|
+
});
|
|
17727
|
+
var VaultEntry = external_exports.object({
|
|
17728
|
+
pointerId: external_exports.string(),
|
|
17729
|
+
// The keyed HMAC of the raw value under `exception.key`, and the epoch it was
|
|
17730
|
+
// derived under. This is what a reveal-to-model grant matches on, and it rotates
|
|
17731
|
+
// independently of the vault encryption key below.
|
|
17732
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17733
|
+
fingerprintKeyVersion: external_exports.number().int().positive(),
|
|
17734
|
+
// The vault-key epoch this row's ciphertext was sealed under.
|
|
17735
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17736
|
+
// Fixed at first mint and never updated: the same value detected later under a
|
|
17737
|
+
// different rule's category keeps the category it was minted with, so one
|
|
17738
|
+
// value always produces exactly one wire token.
|
|
17739
|
+
category: DetectionCategory,
|
|
17740
|
+
ruleId: external_exports.string(),
|
|
17741
|
+
// Partial-reveal preview for badges and listings. Never the raw value.
|
|
17742
|
+
maskedMatch: external_exports.string(),
|
|
17743
|
+
provider: external_exports.string().optional(),
|
|
17744
|
+
ciphertext: external_exports.string(),
|
|
17745
|
+
nonce: external_exports.string(),
|
|
17746
|
+
authTag: external_exports.string(),
|
|
17747
|
+
// How many times this value has been detected on this machine — the reuse
|
|
17748
|
+
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
17749
|
+
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
17750
|
+
firstSeen: external_exports.string(),
|
|
17751
|
+
lastSeen: external_exports.string()
|
|
17752
|
+
});
|
|
17753
|
+
var PointerDescriptor = external_exports.object({
|
|
17754
|
+
category: DetectionCategory,
|
|
17755
|
+
provider: external_exports.string().optional(),
|
|
17756
|
+
maskedMatch: external_exports.string(),
|
|
17757
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17758
|
+
firstSeen: external_exports.string(),
|
|
17759
|
+
lastSeen: external_exports.string()
|
|
17760
|
+
});
|
|
17761
|
+
var PointerIdentity = external_exports.object({
|
|
17762
|
+
ruleId: external_exports.string(),
|
|
17763
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17764
|
+
fingerprintKeyVersion: external_exports.number().int().positive()
|
|
17765
|
+
});
|
|
17766
|
+
var DetokenizeTarget = external_exports.enum(["human", "model"]);
|
|
17767
|
+
var VaultDerefReason = external_exports.enum([
|
|
17768
|
+
"display",
|
|
17769
|
+
"explicit-reveal",
|
|
17770
|
+
"view-render",
|
|
17771
|
+
"model-input",
|
|
17772
|
+
"remediation",
|
|
17773
|
+
"purge"
|
|
17774
|
+
]);
|
|
17775
|
+
var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
|
|
17776
|
+
var BATCHED_DEREF_REASONS = ["display", "view-render"];
|
|
17777
|
+
function isBatchedDerefReason(reason) {
|
|
17778
|
+
return BATCHED_DEREF_REASONS.includes(reason);
|
|
17779
|
+
}
|
|
17780
|
+
var VaultDeref = external_exports.object({
|
|
17781
|
+
id: external_exports.guid(),
|
|
17782
|
+
pointerId: external_exports.string(),
|
|
17783
|
+
at: external_exports.string(),
|
|
17784
|
+
target: DetokenizeTarget,
|
|
17785
|
+
reason: VaultDerefReason,
|
|
17786
|
+
outcome: VaultDerefOutcome,
|
|
17787
|
+
// Present only on a model-target crossing that a reveal grant authorized.
|
|
17788
|
+
grantId: external_exports.string().optional(),
|
|
17789
|
+
// How many pointers ONE batched render resolved. 1 for unbatched rows. Named
|
|
17790
|
+
// apart from VaultEntry.occurrenceCount, which counts detections of a value.
|
|
17791
|
+
pointerCount: external_exports.number().int().positive().default(1)
|
|
17792
|
+
});
|
|
17793
|
+
var VaultSightingKind = external_exports.enum([
|
|
17794
|
+
"prompt",
|
|
17795
|
+
"tool-input",
|
|
17796
|
+
"tool-output",
|
|
17797
|
+
"file",
|
|
17798
|
+
"transcript"
|
|
17799
|
+
]);
|
|
17800
|
+
var VaultSighting = external_exports.object({
|
|
17801
|
+
location: external_exports.string(),
|
|
17802
|
+
kind: VaultSightingKind,
|
|
17803
|
+
firstSeen: external_exports.string(),
|
|
17804
|
+
lastSeen: external_exports.string()
|
|
17805
|
+
});
|
|
17806
|
+
var VaultInventoryEntry = external_exports.object({
|
|
17807
|
+
pointerId: external_exports.string(),
|
|
17808
|
+
category: DetectionCategory,
|
|
17809
|
+
provider: external_exports.string().optional(),
|
|
17810
|
+
maskedMatch: external_exports.string(),
|
|
17811
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17812
|
+
firstSeen: external_exports.string(),
|
|
17813
|
+
lastSeen: external_exports.string(),
|
|
17814
|
+
// The active reveal-to-model grant covering this value, when one exists —
|
|
17815
|
+
// the inventory badges it, the row links to revocation.
|
|
17816
|
+
revealGrantId: external_exports.string().nullable(),
|
|
17817
|
+
sightings: external_exports.array(VaultSighting)
|
|
17818
|
+
});
|
|
17819
|
+
var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
|
|
17820
|
+
var DEFAULT_VAULT_DEREFS_LIMIT = 50;
|
|
17821
|
+
var MAX_VAULT_PAGE_LIMIT = 200;
|
|
17822
|
+
var ListVaultInventoryQuery = external_exports.object({
|
|
17823
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
|
|
17824
|
+
// Opaque; names the last row of the page just served.
|
|
17825
|
+
cursor: external_exports.string().optional()
|
|
17826
|
+
});
|
|
17827
|
+
var ListVaultInventoryResponse = external_exports.object({
|
|
17828
|
+
// Vaulted values across the whole store, not just this page — cursor-
|
|
17829
|
+
// independent, so paging never changes what the count claims.
|
|
17830
|
+
totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
|
|
17831
|
+
items: external_exports.array(VaultInventoryEntry),
|
|
17832
|
+
// `null` once the last page is reached.
|
|
17833
|
+
nextCursor: external_exports.string().nullable()
|
|
17834
|
+
});
|
|
17835
|
+
var ListVaultReuseQuery = external_exports.object({
|
|
17836
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
|
|
17837
|
+
cursor: external_exports.string().optional()
|
|
17838
|
+
});
|
|
17839
|
+
var ListVaultReuseResponse = external_exports.object({
|
|
17840
|
+
// Reused values across the whole store — the number the section's claim
|
|
17841
|
+
// ("values detected in more than one place") is about.
|
|
17842
|
+
totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
|
|
17843
|
+
items: external_exports.array(VaultInventoryEntry),
|
|
17844
|
+
nextCursor: external_exports.string().nullable()
|
|
17845
|
+
});
|
|
17846
|
+
var ListVaultDerefsQuery = external_exports.object({
|
|
17847
|
+
// Include the batched, high-volume reasons (display, view-render). Omitted
|
|
17848
|
+
// hides them and counts them into `hiddenBatched` instead, so the model
|
|
17849
|
+
// crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
|
|
17850
|
+
// over a Server Action, which preserves the type, never as a URL param.
|
|
17851
|
+
includeBatched: external_exports.boolean().optional(),
|
|
17852
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
|
|
17853
|
+
cursor: external_exports.string().optional()
|
|
17854
|
+
});
|
|
17855
|
+
var ListVaultDerefsResponse = external_exports.object({
|
|
17856
|
+
items: external_exports.array(VaultDeref),
|
|
17857
|
+
nextCursor: external_exports.string().nullable(),
|
|
17858
|
+
// Display/view-render rows the query hid, over the WHOLE trail rather than
|
|
17859
|
+
// this page — it is the count the "N hidden" line and its toggle speak for.
|
|
17860
|
+
// Always 0 when `includeBatched` was set, since nothing was hidden.
|
|
17861
|
+
hiddenBatched: external_exports.number().int().nonnegative()
|
|
17862
|
+
});
|
|
17863
|
+
var VaultKeyCustody = external_exports.string();
|
|
17864
|
+
var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
|
|
17865
|
+
var VAULT_CONSENT_VERSION = 1;
|
|
17866
|
+
var VaultConsent = external_exports.object({
|
|
17867
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17868
|
+
version: external_exports.number().int().positive()
|
|
17869
|
+
});
|
|
17870
|
+
function isVaultConsentValid(consent) {
|
|
17871
|
+
return consent?.version === VAULT_CONSENT_VERSION;
|
|
17872
|
+
}
|
|
17873
|
+
|
|
17367
17874
|
// ../../packages/schema/src/zod/local.ts
|
|
17368
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17875
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
|
|
17369
17876
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17370
17877
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17371
17878
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
@@ -17387,6 +17894,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17387
17894
|
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17388
17895
|
// Shares writes.
|
|
17389
17896
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17897
|
+
// Consent to keep a RECOVERABLE encrypted copy of detected values in the local
|
|
17898
|
+
// vault, instead of destroying them. Absent by default: this is a custody
|
|
17899
|
+
// change from one-way redaction, so it is never an assumed grant on upgrade.
|
|
17900
|
+
// Revoking stops future vaulting; it does not erase what is already stored —
|
|
17901
|
+
// purging the vault is the eraser.
|
|
17902
|
+
vaultConsent: VaultConsent.optional(),
|
|
17903
|
+
// Where the vault master key lives.
|
|
17904
|
+
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
17905
|
+
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
17906
|
+
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
17390
17907
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17391
17908
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17392
17909
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -17719,7 +18236,7 @@ var TopSourcesQuery = external_exports.object({
|
|
|
17719
18236
|
// Omit for both kinds.
|
|
17720
18237
|
kind: external_exports.enum(SOURCE_KINDS).optional()
|
|
17721
18238
|
});
|
|
17722
|
-
var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
|
|
18239
|
+
var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
|
|
17723
18240
|
var ScanCoverageProvider = external_exports.object({
|
|
17724
18241
|
provider: Provider,
|
|
17725
18242
|
// Percent of that provider's traffic scanned in the window. 0 when unsupported.
|
|
@@ -17972,6 +18489,195 @@ function captureId(sessionId, contentHash, filePath = null) {
|
|
|
17972
18489
|
);
|
|
17973
18490
|
}
|
|
17974
18491
|
|
|
18492
|
+
// ../../packages/persistence/src/internal/snapshot.ts
|
|
18493
|
+
import { randomUUID } from "crypto";
|
|
18494
|
+
import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
|
|
18495
|
+
import { basename, dirname, join } from "path";
|
|
18496
|
+
|
|
18497
|
+
// ../../packages/persistence/src/paths.ts
|
|
18498
|
+
import {
|
|
18499
|
+
chmodSync,
|
|
18500
|
+
linkSync,
|
|
18501
|
+
lstatSync,
|
|
18502
|
+
mkdirSync,
|
|
18503
|
+
renameSync,
|
|
18504
|
+
rmSync,
|
|
18505
|
+
writeFileSync
|
|
18506
|
+
} from "fs";
|
|
18507
|
+
import { threadId } from "worker_threads";
|
|
18508
|
+
var DATA_DIR_MODE = 448;
|
|
18509
|
+
var DATA_FILE_MODE = 384;
|
|
18510
|
+
var DB_FILENAME = "aka.db";
|
|
18511
|
+
function isSymlink(path) {
|
|
18512
|
+
try {
|
|
18513
|
+
return lstatSync(path).isSymbolicLink();
|
|
18514
|
+
} catch {
|
|
18515
|
+
return false;
|
|
18516
|
+
}
|
|
18517
|
+
}
|
|
18518
|
+
function chmodBestEffort(path, mode) {
|
|
18519
|
+
if (isSymlink(path)) return;
|
|
18520
|
+
try {
|
|
18521
|
+
chmodSync(path, mode);
|
|
18522
|
+
} catch {
|
|
18523
|
+
}
|
|
18524
|
+
}
|
|
18525
|
+
function tightenDir(dir) {
|
|
18526
|
+
chmodBestEffort(dir, DATA_DIR_MODE);
|
|
18527
|
+
}
|
|
18528
|
+
function ensureDataDirSync(dir) {
|
|
18529
|
+
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18530
|
+
tightenDir(dir);
|
|
18531
|
+
}
|
|
18532
|
+
function dbSidecars(file2) {
|
|
18533
|
+
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18534
|
+
}
|
|
18535
|
+
function tightenFile(file2) {
|
|
18536
|
+
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18537
|
+
}
|
|
18538
|
+
function tightenPerms(file2) {
|
|
18539
|
+
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18540
|
+
}
|
|
18541
|
+
function classifyOccupant(file2) {
|
|
18542
|
+
try {
|
|
18543
|
+
if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
|
|
18544
|
+
return { kind: "gone" };
|
|
18545
|
+
} catch (err) {
|
|
18546
|
+
if (err.code === "ENOENT") return { kind: "gone" };
|
|
18547
|
+
return { kind: "unknown", cause: err };
|
|
18548
|
+
}
|
|
18549
|
+
}
|
|
18550
|
+
var KeyUnclaimableError = class extends Error {
|
|
18551
|
+
code = "key-unclaimable";
|
|
18552
|
+
// `cause` is installed only when there IS one. Passing { cause: undefined }
|
|
18553
|
+
// defines the property anyway, so an error carrying nothing would still answer
|
|
18554
|
+
// `'cause' in err` — a present-but-empty field reads as a diagnosis that was
|
|
18555
|
+
// captured and then lost, which is worse than its plain absence.
|
|
18556
|
+
constructor(message, cause) {
|
|
18557
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
18558
|
+
this.name = "KeyUnclaimableError";
|
|
18559
|
+
}
|
|
18560
|
+
};
|
|
18561
|
+
function createOwnerOnlyFileSync(file2, data) {
|
|
18562
|
+
const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
|
|
18563
|
+
try {
|
|
18564
|
+
rmSync(tmp, { force: true });
|
|
18565
|
+
} catch {
|
|
18566
|
+
}
|
|
18567
|
+
let created;
|
|
18568
|
+
try {
|
|
18569
|
+
writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18570
|
+
created = publishByLink(tmp, file2, data);
|
|
18571
|
+
} finally {
|
|
18572
|
+
try {
|
|
18573
|
+
rmSync(tmp, { force: true });
|
|
18574
|
+
} catch {
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
if (created) tightenFile(file2);
|
|
18578
|
+
return created;
|
|
18579
|
+
}
|
|
18580
|
+
var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
|
|
18581
|
+
function publishByLink(tmp, file2, data) {
|
|
18582
|
+
try {
|
|
18583
|
+
linkSync(tmp, file2);
|
|
18584
|
+
return true;
|
|
18585
|
+
} catch (err) {
|
|
18586
|
+
const code = err.code;
|
|
18587
|
+
if (code === "EEXIST") return false;
|
|
18588
|
+
if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
|
|
18589
|
+
}
|
|
18590
|
+
try {
|
|
18591
|
+
writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
|
|
18592
|
+
return true;
|
|
18593
|
+
} catch (err) {
|
|
18594
|
+
if (err.code === "EEXIST") return false;
|
|
18595
|
+
throw err;
|
|
18596
|
+
}
|
|
18597
|
+
}
|
|
18598
|
+
|
|
18599
|
+
// ../../packages/persistence/src/internal/snapshot.ts
|
|
18600
|
+
function backupPath(file2, tag) {
|
|
18601
|
+
return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
|
|
18602
|
+
}
|
|
18603
|
+
var STALE_PARTIAL_MS = 5 * 6e4;
|
|
18604
|
+
function reapStalePartials(file2) {
|
|
18605
|
+
const dir = dirname(file2);
|
|
18606
|
+
const prefix = `${basename(file2)}.`;
|
|
18607
|
+
let entries;
|
|
18608
|
+
try {
|
|
18609
|
+
entries = readdirSync(dir);
|
|
18610
|
+
} catch {
|
|
18611
|
+
return;
|
|
18612
|
+
}
|
|
18613
|
+
for (const name of entries) {
|
|
18614
|
+
if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
|
|
18615
|
+
const partial2 = join(dir, name);
|
|
18616
|
+
try {
|
|
18617
|
+
if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
|
|
18618
|
+
rmSync2(partial2, { force: true });
|
|
18619
|
+
}
|
|
18620
|
+
} catch {
|
|
18621
|
+
}
|
|
18622
|
+
}
|
|
18623
|
+
}
|
|
18624
|
+
function snapshotStore(db, backup) {
|
|
18625
|
+
const partial2 = `${backup}.partial`;
|
|
18626
|
+
try {
|
|
18627
|
+
rmSync2(partial2, { force: true });
|
|
18628
|
+
db.prepare("VACUUM INTO ?").run(partial2);
|
|
18629
|
+
tightenFile(partial2);
|
|
18630
|
+
renameSync2(partial2, backup);
|
|
18631
|
+
} catch (error51) {
|
|
18632
|
+
try {
|
|
18633
|
+
rmSync2(partial2, { force: true });
|
|
18634
|
+
} catch {
|
|
18635
|
+
}
|
|
18636
|
+
throw error51;
|
|
18637
|
+
}
|
|
18638
|
+
}
|
|
18639
|
+
function moveStoreAside(file2, backup) {
|
|
18640
|
+
const undo = [];
|
|
18641
|
+
renameSync2(file2, backup);
|
|
18642
|
+
undo.push([backup, file2]);
|
|
18643
|
+
try {
|
|
18644
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
18645
|
+
const moved = `${backup}${sidecar.slice(file2.length)}`;
|
|
18646
|
+
try {
|
|
18647
|
+
renameSync2(sidecar, moved);
|
|
18648
|
+
undo.push([moved, sidecar]);
|
|
18649
|
+
} catch {
|
|
18650
|
+
rmSync2(sidecar, { force: true });
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
18653
|
+
} catch (error51) {
|
|
18654
|
+
for (const [from, to] of undo.reverse()) {
|
|
18655
|
+
try {
|
|
18656
|
+
renameSync2(from, to);
|
|
18657
|
+
} catch {
|
|
18658
|
+
}
|
|
18659
|
+
}
|
|
18660
|
+
throw error51;
|
|
18661
|
+
}
|
|
18662
|
+
tightenPerms(backup);
|
|
18663
|
+
}
|
|
18664
|
+
function discardStore(file2, backup) {
|
|
18665
|
+
try {
|
|
18666
|
+
rmSync2(file2, { force: true });
|
|
18667
|
+
for (const sidecar of dbSidecars(file2)) {
|
|
18668
|
+
rmSync2(sidecar, { force: true });
|
|
18669
|
+
}
|
|
18670
|
+
} catch (error51) {
|
|
18671
|
+
if (existsSync(file2)) {
|
|
18672
|
+
try {
|
|
18673
|
+
rmSync2(backup, { force: true });
|
|
18674
|
+
} catch {
|
|
18675
|
+
}
|
|
18676
|
+
}
|
|
18677
|
+
throw error51;
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
|
|
17975
18681
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
17976
18682
|
function escapeLikePattern(s) {
|
|
17977
18683
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -18113,44 +18819,12 @@ function mapRowsTolerant(rows, map2) {
|
|
|
18113
18819
|
return out;
|
|
18114
18820
|
}
|
|
18115
18821
|
|
|
18116
|
-
// ../../packages/persistence/src/
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
var DATA_FILE_MODE = 384;
|
|
18120
|
-
var DB_FILENAME = "aka.db";
|
|
18121
|
-
function chmodBestEffort(path, mode) {
|
|
18122
|
-
try {
|
|
18123
|
-
chmodSync(path, mode);
|
|
18124
|
-
} catch {
|
|
18125
|
-
}
|
|
18822
|
+
// ../../packages/persistence/src/migrations.ts
|
|
18823
|
+
function describeObject(object2) {
|
|
18824
|
+
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
18126
18825
|
}
|
|
18127
|
-
function
|
|
18128
|
-
|
|
18129
|
-
}
|
|
18130
|
-
function ensureDataDirSync(dir) {
|
|
18131
|
-
mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
|
|
18132
|
-
tightenDir(dir);
|
|
18133
|
-
}
|
|
18134
|
-
function dbSidecars(file2) {
|
|
18135
|
-
return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
|
|
18136
|
-
}
|
|
18137
|
-
function tightenFile(file2) {
|
|
18138
|
-
try {
|
|
18139
|
-
if (lstatSync(file2).isSymbolicLink()) return;
|
|
18140
|
-
} catch {
|
|
18141
|
-
}
|
|
18142
|
-
chmodBestEffort(file2, DATA_FILE_MODE);
|
|
18143
|
-
}
|
|
18144
|
-
function tightenPerms(file2) {
|
|
18145
|
-
for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
|
|
18146
|
-
}
|
|
18147
|
-
|
|
18148
|
-
// ../../packages/persistence/src/migrations.ts
|
|
18149
|
-
function describeObject(object2) {
|
|
18150
|
-
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
18151
|
-
}
|
|
18152
|
-
function splitStatements(sql) {
|
|
18153
|
-
return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18826
|
+
function splitStatements(sql) {
|
|
18827
|
+
return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18154
18828
|
}
|
|
18155
18829
|
function createdIndexName(statement) {
|
|
18156
18830
|
const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
|
|
@@ -18260,9 +18934,9 @@ function applyLegacyDropMigration(db, file2) {
|
|
|
18260
18934
|
}
|
|
18261
18935
|
}
|
|
18262
18936
|
function backupBeforeLegacyDrop(db, file2) {
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18937
|
+
reapStalePartials(file2);
|
|
18938
|
+
const backup = backupPath(file2, "pre-drop");
|
|
18939
|
+
snapshotStore(db, backup);
|
|
18266
18940
|
return backup;
|
|
18267
18941
|
}
|
|
18268
18942
|
var TOKEN_USAGE_COLUMNS = [
|
|
@@ -18606,6 +19280,25 @@ function parseJsonObject(s) {
|
|
|
18606
19280
|
return void 0;
|
|
18607
19281
|
}
|
|
18608
19282
|
|
|
19283
|
+
// ../../packages/persistence/src/internal/keyset-cursor.ts
|
|
19284
|
+
function encodeKeysetCursor(payload) {
|
|
19285
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
19286
|
+
}
|
|
19287
|
+
function decodeKeysetCursor(cursor) {
|
|
19288
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
19289
|
+
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
|
|
19290
|
+
// resumes from is epoch millis, and a payload carrying ±Infinity or a
|
|
19291
|
+
// fraction binds cleanly rather than failing — returning an EMPTY page with
|
|
19292
|
+
// a null cursor, which a caller reads as "end of list". That is the one
|
|
19293
|
+
// outcome a cursor that does not decode must never produce, since the
|
|
19294
|
+
// documented behaviour above is to restart from the top. (`1e999` is valid
|
|
19295
|
+
// JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
|
|
19296
|
+
Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
|
|
19297
|
+
return parsed;
|
|
19298
|
+
}
|
|
19299
|
+
return null;
|
|
19300
|
+
}
|
|
19301
|
+
|
|
18609
19302
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
18610
19303
|
var DAY_MS = 864e5;
|
|
18611
19304
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
@@ -18651,16 +19344,6 @@ function utcWindow(nowMs) {
|
|
|
18651
19344
|
const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
|
|
18652
19345
|
return { startMs, endMs: startMs + DAY_MS };
|
|
18653
19346
|
}
|
|
18654
|
-
function encodeCursor(payload) {
|
|
18655
|
-
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
18656
|
-
}
|
|
18657
|
-
function decodeCursor(cursor) {
|
|
18658
|
-
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
18659
|
-
if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
|
|
18660
|
-
return parsed;
|
|
18661
|
-
}
|
|
18662
|
-
return null;
|
|
18663
|
-
}
|
|
18664
19347
|
var DB_EVENT_TYPE_TO_KIND = {
|
|
18665
19348
|
session: "session",
|
|
18666
19349
|
prompt: "prompt",
|
|
@@ -18805,7 +19488,7 @@ var SqliteActivityRepository = class {
|
|
|
18805
19488
|
return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
|
|
18806
19489
|
}
|
|
18807
19490
|
listSessions(query) {
|
|
18808
|
-
const cursor = query.cursor ?
|
|
19491
|
+
const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
|
|
18809
19492
|
const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
|
|
18810
19493
|
const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
|
|
18811
19494
|
const conditions = [SESSION_ROOT];
|
|
@@ -18879,7 +19562,7 @@ var SqliteActivityRepository = class {
|
|
|
18879
19562
|
)
|
|
18880
19563
|
);
|
|
18881
19564
|
const last = page[page.length - 1];
|
|
18882
|
-
const nextCursor = hasMore && last ?
|
|
19565
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
|
|
18883
19566
|
return Promise.resolve({ items, nextCursor, emptyCount });
|
|
18884
19567
|
}
|
|
18885
19568
|
getSession(sessionId) {
|
|
@@ -19752,7 +20435,7 @@ var SqliteEventsRepository = class {
|
|
|
19752
20435
|
};
|
|
19753
20436
|
|
|
19754
20437
|
// ../../packages/persistence/src/repositories/exceptions.ts
|
|
19755
|
-
import { randomUUID } from "crypto";
|
|
20438
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
19756
20439
|
|
|
19757
20440
|
// ../../packages/persistence/src/internal/sqlite-errors.ts
|
|
19758
20441
|
var SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
@@ -19784,9 +20467,13 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
19784
20467
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
19785
20468
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
19786
20469
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
20470
|
+
var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
|
|
20471
|
+
AND conditions IS NULL
|
|
20472
|
+
AND ${ACTIVE_PREDICATE}`;
|
|
19787
20473
|
var SqliteExceptionsRepository = class {
|
|
19788
|
-
constructor(db) {
|
|
20474
|
+
constructor(db, now = () => Date.now()) {
|
|
19789
20475
|
this.db = db;
|
|
20476
|
+
this.now = now;
|
|
19790
20477
|
this.consumeStmt = db.prepare(
|
|
19791
20478
|
`UPDATE exceptions
|
|
19792
20479
|
SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
|
|
@@ -19804,6 +20491,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19804
20491
|
);
|
|
19805
20492
|
}
|
|
19806
20493
|
db;
|
|
20494
|
+
now;
|
|
19807
20495
|
consumeStmt;
|
|
19808
20496
|
insertBlockedStmt;
|
|
19809
20497
|
sweepBlockedStmt;
|
|
@@ -19830,8 +20518,8 @@ var SqliteExceptionsRepository = class {
|
|
|
19830
20518
|
"provider conditions are not supported yet \u2014 a grant with one would never apply"
|
|
19831
20519
|
);
|
|
19832
20520
|
}
|
|
19833
|
-
const id =
|
|
19834
|
-
const now =
|
|
20521
|
+
const id = randomUUID2();
|
|
20522
|
+
const now = this.now();
|
|
19835
20523
|
try {
|
|
19836
20524
|
this.insertExceptionRow(id, input, now);
|
|
19837
20525
|
} catch (err) {
|
|
@@ -19875,11 +20563,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19875
20563
|
this.db.prepare(
|
|
19876
20564
|
`INSERT INTO exceptions (
|
|
19877
20565
|
id, rule_id, category, value_fingerprint, key_version, masked_value,
|
|
19878
|
-
scope, expires_at, max_uses, use_count, last_used_at,
|
|
19879
|
-
conditions, created_by, created_via, created_at, updated_at
|
|
20566
|
+
capability, scope, expires_at, max_uses, use_count, last_used_at,
|
|
20567
|
+
justification, conditions, created_by, created_via, created_at, updated_at
|
|
19880
20568
|
) VALUES (
|
|
19881
20569
|
:id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
|
|
19882
|
-
:scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
20570
|
+
:capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
19883
20571
|
:conditions, :createdBy, :createdVia, :now, :now
|
|
19884
20572
|
)`
|
|
19885
20573
|
).run({
|
|
@@ -19889,6 +20577,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19889
20577
|
valueFingerprint: input.valueFingerprint,
|
|
19890
20578
|
keyVersion: input.keyVersion,
|
|
19891
20579
|
maskedValue: input.maskedValue,
|
|
20580
|
+
capability: input.capability ?? "suppress",
|
|
19892
20581
|
scope: input.scope,
|
|
19893
20582
|
expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
|
|
19894
20583
|
maxUses: input.maxUses,
|
|
@@ -19908,7 +20597,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19908
20597
|
const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
|
|
19909
20598
|
const rows = allRows(
|
|
19910
20599
|
this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
|
|
19911
|
-
opts?.includeTerminal ? {} : { now:
|
|
20600
|
+
opts?.includeTerminal ? {} : { now: this.now() }
|
|
19912
20601
|
);
|
|
19913
20602
|
const exceptions = mapRowsTolerant(rows, parseExceptionRow);
|
|
19914
20603
|
return Promise.resolve(exceptions);
|
|
@@ -19943,7 +20632,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19943
20632
|
* already revoked.
|
|
19944
20633
|
*/
|
|
19945
20634
|
revoke(id, revokedBy, reason) {
|
|
19946
|
-
const now =
|
|
20635
|
+
const now = this.now();
|
|
19947
20636
|
const result = this.db.prepare(
|
|
19948
20637
|
`UPDATE exceptions
|
|
19949
20638
|
SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
|
|
@@ -19957,7 +20646,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19957
20646
|
* callers must treat identically — means it does not and the detection is
|
|
19958
20647
|
* enforced as usual. Deliberately NOT wrapped in try/catch.
|
|
19959
20648
|
*/
|
|
19960
|
-
consume(id, now =
|
|
20649
|
+
consume(id, now = this.now()) {
|
|
19961
20650
|
const result = this.consumeStmt.run({ id, now });
|
|
19962
20651
|
return Promise.resolve(Number(result.changes) === 1);
|
|
19963
20652
|
}
|
|
@@ -19966,7 +20655,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19966
20655
|
* version — what rides the policy bundle to the hook. Grants written under
|
|
19967
20656
|
* a different (rotated-away) key never match, so they are excluded at read.
|
|
19968
20657
|
*/
|
|
19969
|
-
activeBundleEntries(keyVersion, now =
|
|
20658
|
+
activeBundleEntries(keyVersion, now = this.now()) {
|
|
19970
20659
|
const rows = allRows(
|
|
19971
20660
|
this.db.prepare(
|
|
19972
20661
|
`SELECT * FROM exceptions
|
|
@@ -19982,6 +20671,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19982
20671
|
ruleId: row.rule_id,
|
|
19983
20672
|
valueFingerprint: row.value_fingerprint,
|
|
19984
20673
|
keyVersion: row.key_version,
|
|
20674
|
+
capability: row.capability,
|
|
19985
20675
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
19986
20676
|
maxUses: row.max_uses,
|
|
19987
20677
|
useCount: row.use_count,
|
|
@@ -19997,7 +20687,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19997
20687
|
* than the retention window on every write, so the ledger self-limits.
|
|
19998
20688
|
*/
|
|
19999
20689
|
recordBlocked(entry) {
|
|
20000
|
-
const now =
|
|
20690
|
+
const now = this.now();
|
|
20001
20691
|
this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
|
|
20002
20692
|
this.insertBlockedStmt.run({
|
|
20003
20693
|
reference: entry.reference,
|
|
@@ -20020,7 +20710,7 @@ var SqliteExceptionsRepository = class {
|
|
|
20020
20710
|
WHERE blocked_at > :cutoff
|
|
20021
20711
|
ORDER BY blocked_at DESC, rowid DESC`
|
|
20022
20712
|
),
|
|
20023
|
-
{ cutoff:
|
|
20713
|
+
{ cutoff: this.now() - windowMs }
|
|
20024
20714
|
);
|
|
20025
20715
|
return Promise.resolve(
|
|
20026
20716
|
rows.map((row) => ({
|
|
@@ -20036,6 +20726,36 @@ var SqliteExceptionsRepository = class {
|
|
|
20036
20726
|
}))
|
|
20037
20727
|
);
|
|
20038
20728
|
}
|
|
20729
|
+
/**
|
|
20730
|
+
* The active reveal-to-model grant for a vaulted value's identity, or null.
|
|
20731
|
+
* Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
|
|
20732
|
+
* suppression uses — plus the capability: a suppression grant must never
|
|
20733
|
+
* authorize a reveal. Read-only: the caller does NOT consume here, because a
|
|
20734
|
+
* revealed value re-enters the detection scan immediately afterward and the
|
|
20735
|
+
* suppression match there claims the use — one crossing, one use.
|
|
20736
|
+
*
|
|
20737
|
+
* A grant with `conditions` NEVER matches here: the reveal path does not yet
|
|
20738
|
+
* evaluate conditions, and a narrowing clause that is ignored would WIDEN the
|
|
20739
|
+
* grant instead. Fail closed until reveal-side condition evaluation exists.
|
|
20740
|
+
*/
|
|
20741
|
+
activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
|
|
20742
|
+
try {
|
|
20743
|
+
const at = now ?? this.now();
|
|
20744
|
+
const row = getRow(
|
|
20745
|
+
this.db.prepare(
|
|
20746
|
+
`SELECT id FROM exceptions
|
|
20747
|
+
WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
|
|
20748
|
+
AND key_version = :keyVersion
|
|
20749
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
20750
|
+
LIMIT 1`
|
|
20751
|
+
),
|
|
20752
|
+
{ ruleId, valueFingerprint, keyVersion, now: at }
|
|
20753
|
+
);
|
|
20754
|
+
return Promise.resolve(row ?? null);
|
|
20755
|
+
} catch (err) {
|
|
20756
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
20757
|
+
}
|
|
20758
|
+
}
|
|
20039
20759
|
/**
|
|
20040
20760
|
* Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
|
|
20041
20761
|
* exhausted) whose last transition is older than the retention window.
|
|
@@ -20043,7 +20763,7 @@ var SqliteExceptionsRepository = class {
|
|
|
20043
20763
|
* predicate, so correctness never depends on this sweep; it only bounds how
|
|
20044
20764
|
* long the audit evidence is kept locally. Returns the deleted count.
|
|
20045
20765
|
*/
|
|
20046
|
-
sweepTerminal(retentionMs, now =
|
|
20766
|
+
sweepTerminal(retentionMs, now = this.now()) {
|
|
20047
20767
|
const result = this.db.prepare(
|
|
20048
20768
|
`DELETE FROM exceptions
|
|
20049
20769
|
WHERE updated_at < :cutoff
|
|
@@ -20063,6 +20783,7 @@ function parseExceptionRow(row) {
|
|
|
20063
20783
|
valueFingerprint: row.value_fingerprint,
|
|
20064
20784
|
keyVersion: row.key_version,
|
|
20065
20785
|
maskedValue: row.masked_value,
|
|
20786
|
+
capability: row.capability,
|
|
20066
20787
|
scope: row.scope,
|
|
20067
20788
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20068
20789
|
maxUses: row.max_uses,
|
|
@@ -20105,6 +20826,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
20105
20826
|
|
|
20106
20827
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
20107
20828
|
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
20829
|
+
var SCAN_BATCH_ROWS = 1e3;
|
|
20830
|
+
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
20831
|
+
var LOCATION_RULE_IDS_CAP = 20;
|
|
20832
|
+
function compareLocationOrder(a, b) {
|
|
20833
|
+
return compareFindingGroupOrder(
|
|
20834
|
+
{
|
|
20835
|
+
severity: a.maxSeverity,
|
|
20836
|
+
latestDetectedAt: a.latestDetectedAt,
|
|
20837
|
+
id: ""
|
|
20838
|
+
},
|
|
20839
|
+
{
|
|
20840
|
+
severity: b.maxSeverity,
|
|
20841
|
+
latestDetectedAt: b.latestDetectedAt,
|
|
20842
|
+
id: ""
|
|
20843
|
+
}
|
|
20844
|
+
);
|
|
20845
|
+
}
|
|
20108
20846
|
var CONCAT_SEP = ",";
|
|
20109
20847
|
var TUPLE_SEP = "|";
|
|
20110
20848
|
function splitConcat(value) {
|
|
@@ -20117,6 +20855,33 @@ function deriveInstanceStatus(row) {
|
|
|
20117
20855
|
latestResolutionStatus: row.latest_status
|
|
20118
20856
|
});
|
|
20119
20857
|
}
|
|
20858
|
+
function encodeGroupCursor(group) {
|
|
20859
|
+
const payload = {
|
|
20860
|
+
sev: group.severity,
|
|
20861
|
+
t: group.latestDetectedAt,
|
|
20862
|
+
id: group.id
|
|
20863
|
+
};
|
|
20864
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
20865
|
+
}
|
|
20866
|
+
function decodeGroupCursor(cursor) {
|
|
20867
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
20868
|
+
if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
|
|
20869
|
+
return {
|
|
20870
|
+
severity: parsed.sev,
|
|
20871
|
+
latestDetectedAt: parsed.t,
|
|
20872
|
+
id: parsed.id
|
|
20873
|
+
};
|
|
20874
|
+
}
|
|
20875
|
+
return null;
|
|
20876
|
+
}
|
|
20877
|
+
function firstAfter(sorted, cursor) {
|
|
20878
|
+
const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
|
|
20879
|
+
return index === -1 ? sorted.length : index;
|
|
20880
|
+
}
|
|
20881
|
+
function findDeepLinked(sorted, page, id) {
|
|
20882
|
+
if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
|
|
20883
|
+
return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
|
|
20884
|
+
}
|
|
20120
20885
|
var DAY_MS3 = 864e5;
|
|
20121
20886
|
var SqliteFindingsRepository = class {
|
|
20122
20887
|
constructor(db) {
|
|
@@ -20226,8 +20991,13 @@ var SqliteFindingsRepository = class {
|
|
|
20226
20991
|
*/
|
|
20227
20992
|
listGroupedFindings(query) {
|
|
20228
20993
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
20229
|
-
const
|
|
20230
|
-
const
|
|
20994
|
+
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
20995
|
+
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
20996
|
+
const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
|
|
20997
|
+
const sessionParams = {
|
|
20998
|
+
...query.sessionId ? { sessionId: query.sessionId } : {},
|
|
20999
|
+
...fromMs === void 0 ? {} : { fromMs }
|
|
21000
|
+
};
|
|
20231
21001
|
const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
|
|
20232
21002
|
predicate,
|
|
20233
21003
|
params: sessionParams
|
|
@@ -20235,7 +21005,8 @@ var SqliteFindingsRepository = class {
|
|
|
20235
21005
|
const rows = allRows(
|
|
20236
21006
|
this.db.prepare(
|
|
20237
21007
|
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
20238
|
-
occurred_at, source_tool, repo, file, tool_name,
|
|
21008
|
+
occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
|
|
21009
|
+
kind, finding_key, latest_status
|
|
20239
21010
|
FROM (
|
|
20240
21011
|
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
20241
21012
|
d.severity AS severity, f.masked_match AS masked_match,
|
|
@@ -20245,6 +21016,7 @@ var SqliteFindingsRepository = class {
|
|
|
20245
21016
|
json_extract(e.attributes, '$.repo') AS repo,
|
|
20246
21017
|
json_extract(e.attributes, '$.file_path') AS file,
|
|
20247
21018
|
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
21019
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
20248
21020
|
e.event_type AS kind, f.finding_key AS finding_key,
|
|
20249
21021
|
latest.status AS latest_status,
|
|
20250
21022
|
ROW_NUMBER() OVER (
|
|
@@ -20276,6 +21048,8 @@ var SqliteFindingsRepository = class {
|
|
|
20276
21048
|
repo: r.repo ?? "",
|
|
20277
21049
|
file: r.file ?? "",
|
|
20278
21050
|
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
21051
|
+
eventId: r.event_id,
|
|
21052
|
+
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
20279
21053
|
status: deriveInstanceStatus(r)
|
|
20280
21054
|
}));
|
|
20281
21055
|
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
@@ -20299,18 +21073,23 @@ var SqliteFindingsRepository = class {
|
|
|
20299
21073
|
groups: sorted.length
|
|
20300
21074
|
};
|
|
20301
21075
|
const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
|
|
21076
|
+
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
21077
|
+
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
21078
|
+
const page = sorted.slice(start, start + limit);
|
|
21079
|
+
const lastOnPage = page.at(-1);
|
|
21080
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
21081
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
20302
21082
|
const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
|
|
20303
|
-
const
|
|
20304
|
-
|
|
20305
|
-
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
);
|
|
21083
|
+
const narrow = (g) => statusSet ? {
|
|
21084
|
+
...g,
|
|
21085
|
+
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
21086
|
+
} : g;
|
|
21087
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
20309
21088
|
return Promise.resolve({
|
|
20310
21089
|
totals,
|
|
20311
21090
|
facets,
|
|
20312
21091
|
items,
|
|
20313
|
-
nextCursor
|
|
21092
|
+
nextCursor,
|
|
20314
21093
|
...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
|
|
20315
21094
|
});
|
|
20316
21095
|
}
|
|
@@ -20342,6 +21121,266 @@ var SqliteFindingsRepository = class {
|
|
|
20342
21121
|
* request actually carries a `q`. (Substring matching is unaffected by a
|
|
20343
21122
|
* path repeating across tuples.)
|
|
20344
21123
|
*/
|
|
21124
|
+
/**
|
|
21125
|
+
* The instance-level (flat) findings list: one row per finding, newest first,
|
|
21126
|
+
* paged by keyset.
|
|
21127
|
+
*
|
|
21128
|
+
* SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
|
|
21129
|
+
* bound are SQL predicates: nothing counts them, so narrowing the scan by
|
|
21130
|
+
* them changes no reported number. Severity, subtype, provider, action,
|
|
21131
|
+
* status, tool, repo, file and `q` all stay in JS — each has a facet, and a
|
|
21132
|
+
* facet excludes its own filter, so a row the filter rejects still has to be
|
|
21133
|
+
* counted. Pushing any of them into SQL would silently empty its own facet.
|
|
21134
|
+
* Several could not be expressed there anyway: status comes from the one
|
|
21135
|
+
* shared classifier (deriveFindingStatus), and provider 'api' means "a tool
|
|
21136
|
+
* none of the mappers names", which no IN-list can say.
|
|
21137
|
+
*
|
|
21138
|
+
* The scan runs from the top of the scope on every request, not from the
|
|
21139
|
+
* cursor: `totals` and `facets` describe the whole filtered scope and must not
|
|
21140
|
+
* move as the caller pages. Rows are pulled in batches so memory stays flat
|
|
21141
|
+
* while the counting runs, and only the page itself is retained.
|
|
21142
|
+
*/
|
|
21143
|
+
listFindingInstances(query) {
|
|
21144
|
+
const opts = {
|
|
21145
|
+
severity: query.severity,
|
|
21146
|
+
subtype: query.subtype,
|
|
21147
|
+
providers: query.provider,
|
|
21148
|
+
actions: query.action,
|
|
21149
|
+
statuses: query.status,
|
|
21150
|
+
tools: query.tool,
|
|
21151
|
+
repo: query.repo,
|
|
21152
|
+
file: query.file,
|
|
21153
|
+
q: query.q
|
|
21154
|
+
};
|
|
21155
|
+
const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
|
|
21156
|
+
const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
|
|
21157
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
21158
|
+
const items = [];
|
|
21159
|
+
let total = 0;
|
|
21160
|
+
let last;
|
|
21161
|
+
let hasMore = false;
|
|
21162
|
+
for (const row of this.scanFindingRows({
|
|
21163
|
+
sessionId: query.sessionId,
|
|
21164
|
+
from: query.from
|
|
21165
|
+
})) {
|
|
21166
|
+
accumulator.add(row);
|
|
21167
|
+
if (!matchesInstanceFilters(row, opts)) continue;
|
|
21168
|
+
total += 1;
|
|
21169
|
+
if (items.length < limit) {
|
|
21170
|
+
items.push(toInstanceDetail(row));
|
|
21171
|
+
last = row;
|
|
21172
|
+
} else {
|
|
21173
|
+
hasMore = true;
|
|
21174
|
+
}
|
|
21175
|
+
}
|
|
21176
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
|
|
21177
|
+
if (cursor !== null) {
|
|
21178
|
+
const resumed = this.pageAfter(cursor, opts, limit, query);
|
|
21179
|
+
return Promise.resolve({
|
|
21180
|
+
totals: { findings: total },
|
|
21181
|
+
facets: accumulator.facets(),
|
|
21182
|
+
items: resumed.items,
|
|
21183
|
+
nextCursor: resumed.nextCursor
|
|
21184
|
+
});
|
|
21185
|
+
}
|
|
21186
|
+
return Promise.resolve({
|
|
21187
|
+
totals: { findings: total },
|
|
21188
|
+
facets: accumulator.facets(),
|
|
21189
|
+
items,
|
|
21190
|
+
nextCursor
|
|
21191
|
+
});
|
|
21192
|
+
}
|
|
21193
|
+
/**
|
|
21194
|
+
* The page of matching rows strictly after `cursor`. Separate from the
|
|
21195
|
+
* counting pass because that one starts at the top of the scope by design;
|
|
21196
|
+
* this one narrows the scan with the same keyset predicate the activity list
|
|
21197
|
+
* uses, so a later page costs less than the first rather than more.
|
|
21198
|
+
*/
|
|
21199
|
+
pageAfter(cursor, opts, limit, query) {
|
|
21200
|
+
const items = [];
|
|
21201
|
+
let last;
|
|
21202
|
+
let hasMore = false;
|
|
21203
|
+
for (const row of this.scanFindingRows({
|
|
21204
|
+
sessionId: query.sessionId,
|
|
21205
|
+
from: query.from,
|
|
21206
|
+
after: cursor
|
|
21207
|
+
})) {
|
|
21208
|
+
if (!matchesInstanceFilters(row, opts)) continue;
|
|
21209
|
+
if (items.length < limit) {
|
|
21210
|
+
items.push(toInstanceDetail(row));
|
|
21211
|
+
last = row;
|
|
21212
|
+
} else {
|
|
21213
|
+
hasMore = true;
|
|
21214
|
+
break;
|
|
21215
|
+
}
|
|
21216
|
+
}
|
|
21217
|
+
return {
|
|
21218
|
+
items,
|
|
21219
|
+
nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
|
|
21220
|
+
};
|
|
21221
|
+
}
|
|
21222
|
+
/**
|
|
21223
|
+
* The same findings folded by location: repository, then file within it.
|
|
21224
|
+
*
|
|
21225
|
+
* The grouping keys come from the capturing event's attributes, which is what
|
|
21226
|
+
* the local store relates a finding to — there is no finding↔asset row to
|
|
21227
|
+
* group by instead. A repo or file the event did not record folds into the
|
|
21228
|
+
* empty-string bucket, which the view renders but does not link, since no
|
|
21229
|
+
* filter can name it.
|
|
21230
|
+
*/
|
|
21231
|
+
listFindingLocations(query) {
|
|
21232
|
+
const opts = {
|
|
21233
|
+
severity: query.severity,
|
|
21234
|
+
subtype: query.subtype,
|
|
21235
|
+
providers: query.provider,
|
|
21236
|
+
actions: query.action,
|
|
21237
|
+
statuses: query.status,
|
|
21238
|
+
tools: query.tool,
|
|
21239
|
+
q: query.q
|
|
21240
|
+
};
|
|
21241
|
+
const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
|
|
21242
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
21243
|
+
let total = 0;
|
|
21244
|
+
for (const row of this.scanFindingRows({
|
|
21245
|
+
sessionId: query.sessionId,
|
|
21246
|
+
from: query.from
|
|
21247
|
+
})) {
|
|
21248
|
+
if (!matchesInstanceFilters(row, opts)) continue;
|
|
21249
|
+
total += 1;
|
|
21250
|
+
let files = byRepo.get(row.repo);
|
|
21251
|
+
if (files === void 0) {
|
|
21252
|
+
files = /* @__PURE__ */ new Map();
|
|
21253
|
+
byRepo.set(row.repo, files);
|
|
21254
|
+
}
|
|
21255
|
+
let acc = files.get(row.file);
|
|
21256
|
+
if (acc === void 0) {
|
|
21257
|
+
acc = newLocationAccumulator();
|
|
21258
|
+
files.set(row.file, acc);
|
|
21259
|
+
}
|
|
21260
|
+
addToLocation(acc, row);
|
|
21261
|
+
}
|
|
21262
|
+
let fileCount = 0;
|
|
21263
|
+
const repos = [...byRepo.entries()].map(([repo, files]) => {
|
|
21264
|
+
fileCount += files.size;
|
|
21265
|
+
const fileRows = [...files.entries()].map(([file2, acc]) => ({
|
|
21266
|
+
file: file2,
|
|
21267
|
+
instanceCount: acc.instanceCount,
|
|
21268
|
+
maxSeverity: acc.maxSeverity,
|
|
21269
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
21270
|
+
...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
|
|
21271
|
+
ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
|
|
21272
|
+
})).sort(compareLocationOrder);
|
|
21273
|
+
const rollup = fileRows.reduce(
|
|
21274
|
+
(a, f) => ({
|
|
21275
|
+
instanceCount: a.instanceCount + f.instanceCount,
|
|
21276
|
+
maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
|
|
21277
|
+
latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
|
|
21278
|
+
}),
|
|
21279
|
+
{
|
|
21280
|
+
instanceCount: 0,
|
|
21281
|
+
maxSeverity: fileRows[0]?.maxSeverity ?? "low",
|
|
21282
|
+
latestDetectedAt: ""
|
|
21283
|
+
}
|
|
21284
|
+
);
|
|
21285
|
+
const statuses = fileRows.map((f) => f.status);
|
|
21286
|
+
const folded = foldGroupStatus(statuses);
|
|
21287
|
+
return {
|
|
21288
|
+
repo,
|
|
21289
|
+
instanceCount: rollup.instanceCount,
|
|
21290
|
+
maxSeverity: rollup.maxSeverity,
|
|
21291
|
+
latestDetectedAt: rollup.latestDetectedAt,
|
|
21292
|
+
...folded === void 0 ? {} : { status: folded },
|
|
21293
|
+
files: fileRows
|
|
21294
|
+
};
|
|
21295
|
+
});
|
|
21296
|
+
repos.sort(compareLocationOrder);
|
|
21297
|
+
return Promise.resolve({
|
|
21298
|
+
totals: { findings: total, repos: repos.length, files: fileCount },
|
|
21299
|
+
items: repos.slice(0, limit),
|
|
21300
|
+
hasMore: repos.length > limit
|
|
21301
|
+
});
|
|
21302
|
+
}
|
|
21303
|
+
/**
|
|
21304
|
+
* Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
|
|
21305
|
+
*
|
|
21306
|
+
* A generator so a caller streams the scope without it ever being an array:
|
|
21307
|
+
* the flat list counts and facets the whole filtered scope, which on a large
|
|
21308
|
+
* store is far more rows than any page. Each batch advances the same keyset
|
|
21309
|
+
* predicate the page read uses, so the scan is a sequence of bounded reads
|
|
21310
|
+
* rather than one unbounded result set.
|
|
21311
|
+
*
|
|
21312
|
+
* The latest-resolution lookup is the CORRELATED form, not the derived table
|
|
21313
|
+
* the grouped path joins: only `status` is needed, idx_finding_resolution_key
|
|
21314
|
+
* makes it a point lookup per row, and the derived table would re-materialize
|
|
21315
|
+
* a window over the whole resolution table once per batch.
|
|
21316
|
+
*
|
|
21317
|
+
* `scope` carries ONLY what no facet counts. A filter dimension narrowed here
|
|
21318
|
+
* would be missing from its own facet, which is computed by excluding that
|
|
21319
|
+
* dimension — see listFindingInstances.
|
|
21320
|
+
*/
|
|
21321
|
+
*scanFindingRows(scope) {
|
|
21322
|
+
const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
|
|
21323
|
+
const params = [];
|
|
21324
|
+
if (scope.sessionId !== void 0 && scope.sessionId !== "") {
|
|
21325
|
+
conditions.push("e.root_session_id = ?");
|
|
21326
|
+
params.push(scope.sessionId);
|
|
21327
|
+
}
|
|
21328
|
+
if (scope.from !== void 0) {
|
|
21329
|
+
conditions.push("e.started_at >= ?");
|
|
21330
|
+
params.push(isoToEpochMillis(scope.from));
|
|
21331
|
+
}
|
|
21332
|
+
const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
21333
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
21334
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
21335
|
+
e.started_at AS occurred_at,
|
|
21336
|
+
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
21337
|
+
json_extract(e.attributes, '$.repo') AS repo,
|
|
21338
|
+
json_extract(e.attributes, '$.file_path') AS file,
|
|
21339
|
+
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
21340
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
21341
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
21342
|
+
${latestResolutionStatusSql("f")} AS latest_status
|
|
21343
|
+
FROM inspection_findings f
|
|
21344
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
21345
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
21346
|
+
WHERE ${conditions.join(" AND ")}
|
|
21347
|
+
AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
|
|
21348
|
+
ORDER BY e.started_at DESC, f.id DESC
|
|
21349
|
+
LIMIT ?`;
|
|
21350
|
+
let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
|
|
21351
|
+
for (; ; ) {
|
|
21352
|
+
const rows = allRows(this.db.prepare(sql), [
|
|
21353
|
+
...params,
|
|
21354
|
+
after.startedAtMs,
|
|
21355
|
+
after.startedAtMs,
|
|
21356
|
+
after.id,
|
|
21357
|
+
SCAN_BATCH_ROWS
|
|
21358
|
+
]);
|
|
21359
|
+
for (const r of rows) {
|
|
21360
|
+
yield {
|
|
21361
|
+
id: r.id,
|
|
21362
|
+
ruleId: r.rule_id,
|
|
21363
|
+
category: r.category,
|
|
21364
|
+
severity: r.severity,
|
|
21365
|
+
maskedMatch: r.masked_match,
|
|
21366
|
+
actionTaken: r.action_taken,
|
|
21367
|
+
confidence: r.confidence,
|
|
21368
|
+
occurredAt: epochMillisToIso(r.occurred_at),
|
|
21369
|
+
sourceTool: r.source_tool,
|
|
21370
|
+
repo: r.repo ?? "",
|
|
21371
|
+
file: r.file ?? "",
|
|
21372
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
21373
|
+
eventId: r.event_id,
|
|
21374
|
+
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
21375
|
+
status: deriveInstanceStatus(r)
|
|
21376
|
+
};
|
|
21377
|
+
}
|
|
21378
|
+
if (rows.length < SCAN_BATCH_ROWS) return;
|
|
21379
|
+
const lastRow = rows[rows.length - 1];
|
|
21380
|
+
if (lastRow === void 0) return;
|
|
21381
|
+
after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
|
|
21382
|
+
}
|
|
21383
|
+
}
|
|
20345
21384
|
groupAggregates(withSearchText, scope) {
|
|
20346
21385
|
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
|
|
20347
21386
|
group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
|
|
@@ -20602,7 +21641,7 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
20602
21641
|
};
|
|
20603
21642
|
|
|
20604
21643
|
// ../../packages/persistence/src/repositories/installed-packs.ts
|
|
20605
|
-
import { createHash as createHash2, randomUUID as
|
|
21644
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
20606
21645
|
|
|
20607
21646
|
// ../../packages/persistence/src/semver.ts
|
|
20608
21647
|
function parse3(version2) {
|
|
@@ -20753,7 +21792,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20753
21792
|
let behind = false;
|
|
20754
21793
|
for (const row of rows) {
|
|
20755
21794
|
const params = {
|
|
20756
|
-
id:
|
|
21795
|
+
id: randomUUID3(),
|
|
20757
21796
|
namespace: row.namespace,
|
|
20758
21797
|
packId: row.packId,
|
|
20759
21798
|
version: row.version,
|
|
@@ -20765,7 +21804,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
20765
21804
|
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
20766
21805
|
this.upsertAvailableStmt.run({
|
|
20767
21806
|
...params,
|
|
20768
|
-
id:
|
|
21807
|
+
id: randomUUID3(),
|
|
20769
21808
|
recordedBy: meta3?.recordedBy ?? null
|
|
20770
21809
|
});
|
|
20771
21810
|
} else {
|
|
@@ -21088,14 +22127,15 @@ var SqliteInventoryRepository = class {
|
|
|
21088
22127
|
};
|
|
21089
22128
|
|
|
21090
22129
|
// ../../packages/persistence/src/repositories/inventory-assets.ts
|
|
21091
|
-
import { randomUUID as
|
|
22130
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
21092
22131
|
var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
|
|
21093
22132
|
var VALID_HARNESS_IDS = new Set(HarnessId.options);
|
|
21094
22133
|
var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
|
|
21095
22134
|
var HARNESS_LABELS = {
|
|
21096
22135
|
claudecode: "Claude Code",
|
|
21097
22136
|
cursor: "Cursor",
|
|
21098
|
-
codex: "Codex"
|
|
22137
|
+
codex: "Codex",
|
|
22138
|
+
antigravity: "Antigravity"
|
|
21099
22139
|
};
|
|
21100
22140
|
var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
21101
22141
|
var EMPTY_PROJECT_AGG = {
|
|
@@ -21110,6 +22150,7 @@ function resolveHarnessId(attrs, row) {
|
|
|
21110
22150
|
if (t.includes("claudecode") || t === "claude") return "claudecode";
|
|
21111
22151
|
if (t.includes("cursor")) return "cursor";
|
|
21112
22152
|
if (t.includes("codex")) return "codex";
|
|
22153
|
+
if (t.includes("antigravity")) return "antigravity";
|
|
21113
22154
|
return null;
|
|
21114
22155
|
}
|
|
21115
22156
|
function isLiveRealClaudeCode(rows) {
|
|
@@ -21568,7 +22609,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21568
22609
|
`INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
|
|
21569
22610
|
VALUES (:id, :projectId, :path, :access, :now, :now)
|
|
21570
22611
|
ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
|
|
21571
|
-
).run({ id:
|
|
22612
|
+
).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
|
|
21572
22613
|
}
|
|
21573
22614
|
return true;
|
|
21574
22615
|
}
|
|
@@ -21589,7 +22630,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21589
22630
|
`INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
|
|
21590
22631
|
VALUES (:id, :assetId, :trust, :now, :now)
|
|
21591
22632
|
ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
|
|
21592
|
-
).run({ id:
|
|
22633
|
+
).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
|
|
21593
22634
|
}
|
|
21594
22635
|
this.configRowsCache = void 0;
|
|
21595
22636
|
return "ok";
|
|
@@ -21886,7 +22927,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
21886
22927
|
};
|
|
21887
22928
|
|
|
21888
22929
|
// ../../packages/persistence/src/repositories/policies.ts
|
|
21889
|
-
import { randomUUID as
|
|
22930
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
21890
22931
|
var SqlitePoliciesRepository = class {
|
|
21891
22932
|
constructor(db) {
|
|
21892
22933
|
this.db = db;
|
|
@@ -21921,7 +22962,7 @@ var SqlitePoliciesRepository = class {
|
|
|
21921
22962
|
failOpenTransaction(this.db, () => {
|
|
21922
22963
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
21923
22964
|
stmt.run({
|
|
21924
|
-
id:
|
|
22965
|
+
id: randomUUID5(),
|
|
21925
22966
|
target: JSON.stringify({ category }),
|
|
21926
22967
|
action,
|
|
21927
22968
|
now: Date.now()
|
|
@@ -21941,7 +22982,7 @@ var SqlitePoliciesRepository = class {
|
|
|
21941
22982
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
21942
22983
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
21943
22984
|
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
21944
|
-
).run({ id:
|
|
22985
|
+
).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
|
|
21945
22986
|
}
|
|
21946
22987
|
// Caps every global per-category policy currently set to block/redact down
|
|
21947
22988
|
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
@@ -22009,7 +23050,7 @@ var SqlitePolicyCatalogRepository = class {
|
|
|
22009
23050
|
};
|
|
22010
23051
|
|
|
22011
23052
|
// ../../packages/persistence/src/repositories/project-files.ts
|
|
22012
|
-
import { randomUUID as
|
|
23053
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
22013
23054
|
var SqliteProjectFilesRepository = class {
|
|
22014
23055
|
constructor(db) {
|
|
22015
23056
|
this.db = db;
|
|
@@ -22041,7 +23082,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
22041
23082
|
const stamp = Math.max(now, maxStamp + 1);
|
|
22042
23083
|
for (const file2 of scan2.files) {
|
|
22043
23084
|
this.upsertStmt.run({
|
|
22044
|
-
id:
|
|
23085
|
+
id: randomUUID6(),
|
|
22045
23086
|
projectId,
|
|
22046
23087
|
path: file2.path,
|
|
22047
23088
|
name: file2.name,
|
|
@@ -22055,7 +23096,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
22055
23096
|
};
|
|
22056
23097
|
|
|
22057
23098
|
// ../../packages/persistence/src/repositories/resolutions.ts
|
|
22058
|
-
import { randomUUID as
|
|
23099
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
22059
23100
|
var SqliteResolutionsRepository = class {
|
|
22060
23101
|
constructor(db, now = () => Date.now()) {
|
|
22061
23102
|
this.db = db;
|
|
@@ -22109,7 +23150,7 @@ var SqliteResolutionsRepository = class {
|
|
|
22109
23150
|
*/
|
|
22110
23151
|
insertResolution(r) {
|
|
22111
23152
|
this.insertStmt.run({
|
|
22112
|
-
id:
|
|
23153
|
+
id: randomUUID7(),
|
|
22113
23154
|
findingKey: r.findingKey,
|
|
22114
23155
|
status: FindingStatus.parse(r.status),
|
|
22115
23156
|
method: ResolutionMethod.parse(r.method),
|
|
@@ -22168,13 +23209,51 @@ var SqliteRuleProbeCacheRepository = class {
|
|
|
22168
23209
|
this.readStmt = db.prepare(
|
|
22169
23210
|
`SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
|
|
22170
23211
|
);
|
|
23212
|
+
this.countQuarantinedStmt = db.prepare(
|
|
23213
|
+
`SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
|
|
23214
|
+
);
|
|
23215
|
+
this.clearQuarantinedStmt = db.prepare(
|
|
23216
|
+
`DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
|
|
23217
|
+
);
|
|
22171
23218
|
}
|
|
22172
23219
|
db;
|
|
22173
23220
|
upsertStmt;
|
|
22174
23221
|
readStmt;
|
|
23222
|
+
countQuarantinedStmt;
|
|
23223
|
+
clearQuarantinedStmt;
|
|
22175
23224
|
getVerdict(ruleKey) {
|
|
22176
23225
|
return getRow(this.readStmt, { ruleKey });
|
|
22177
23226
|
}
|
|
23227
|
+
/** How many rules are currently excluded by a cached quarantine verdict. */
|
|
23228
|
+
countQuarantined() {
|
|
23229
|
+
return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
|
|
23230
|
+
}
|
|
23231
|
+
/**
|
|
23232
|
+
* Forgets every quarantine verdict, so the rules behind them are measured
|
|
23233
|
+
* again on the next load. This is the undo for a verdict the machine reached
|
|
23234
|
+
* on its own: a rule terminated mid-scan is cached forever and dropped from
|
|
23235
|
+
* every later scan, and a timing verdict is a wall-clock judgement that a
|
|
23236
|
+
* loaded or slow machine can reach about a rule that is in fact fine.
|
|
23237
|
+
*
|
|
23238
|
+
* Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
|
|
23239
|
+
* keeping, and dropping it would make every rule pay the battery again.
|
|
23240
|
+
*
|
|
23241
|
+
* Reports `refused` from the write's own result rather than inferring it from
|
|
23242
|
+
* the row count. The two are NOT the same answer: `failOpenTransaction`
|
|
23243
|
+
* swallows a contended DELETE (another writer holding the lock past
|
|
23244
|
+
* `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
|
|
23245
|
+
* leaves the count unchanged, which is indistinguishable from "there was
|
|
23246
|
+
* nothing to clear". An undo that reports success while the quarantines are
|
|
23247
|
+
* still in place is worse than one that fails, because the rules it claimed
|
|
23248
|
+
* to restore are silently still disabled.
|
|
23249
|
+
*/
|
|
23250
|
+
clearQuarantined() {
|
|
23251
|
+
const before = this.countQuarantined();
|
|
23252
|
+
const committed = failOpenTransaction(this.db, () => {
|
|
23253
|
+
this.clearQuarantinedStmt.run();
|
|
23254
|
+
});
|
|
23255
|
+
return { refused: !committed, cleared: before - this.countQuarantined() };
|
|
23256
|
+
}
|
|
22178
23257
|
setVerdict(ruleKey, verdict, worstProbeMs) {
|
|
22179
23258
|
failOpenTransaction(this.db, () => {
|
|
22180
23259
|
this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
|
|
@@ -22228,92 +23307,507 @@ var SqliteScanLedgerRepository = class {
|
|
|
22228
23307
|
}
|
|
22229
23308
|
};
|
|
22230
23309
|
|
|
22231
|
-
// ../../packages/persistence/src/repositories/
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
redact: "redacted",
|
|
22237
|
-
warn: "warned"
|
|
22238
|
-
};
|
|
22239
|
-
var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
|
|
22240
|
-
var SCAN_COVERAGE = [
|
|
22241
|
-
{ provider: "claudecode", coverage: 100, supported: true },
|
|
22242
|
-
{ provider: "cursor", coverage: 0, supported: false },
|
|
22243
|
-
{ provider: "codex", coverage: 0, supported: false },
|
|
22244
|
-
{ provider: "chatgpt", coverage: 0, supported: false },
|
|
22245
|
-
{ provider: "copilot", coverage: 0, supported: false },
|
|
22246
|
-
{ provider: "api", coverage: 0, supported: false }
|
|
22247
|
-
];
|
|
22248
|
-
var GRANULARITY = {
|
|
22249
|
-
"7d": "day",
|
|
22250
|
-
"30d": "day",
|
|
22251
|
-
"3m": "week",
|
|
22252
|
-
"6m": "week"
|
|
22253
|
-
};
|
|
22254
|
-
function granularityFor(range) {
|
|
22255
|
-
return GRANULARITY[range];
|
|
22256
|
-
}
|
|
22257
|
-
function startOfUtcDay2(ms) {
|
|
22258
|
-
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
23310
|
+
// ../../packages/persistence/src/repositories/secret-vault.ts
|
|
23311
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
23312
|
+
function pageLimit(requested, fallback) {
|
|
23313
|
+
if (requested === void 0) return fallback;
|
|
23314
|
+
return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
|
|
22259
23315
|
}
|
|
22260
|
-
function
|
|
22261
|
-
return
|
|
23316
|
+
function encodeReuseCursor(payload) {
|
|
23317
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
22262
23318
|
}
|
|
22263
|
-
function
|
|
22264
|
-
|
|
23319
|
+
function decodeReuseCursor(cursor) {
|
|
23320
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
23321
|
+
if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
|
|
23322
|
+
// ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
|
|
23323
|
+
// null cursor, which the caller reads as "end of list" — the one outcome a
|
|
23324
|
+
// malformed cursor must never produce, since restarting from the top is the
|
|
23325
|
+
// documented behaviour and the only recoverable one. (`1e999` is valid JSON
|
|
23326
|
+
// and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
|
|
23327
|
+
Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
|
|
23328
|
+
return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
|
|
23329
|
+
}
|
|
23330
|
+
return null;
|
|
22265
23331
|
}
|
|
22266
|
-
var
|
|
22267
|
-
|
|
23332
|
+
var REUSED_PREDICATE = `(v.occurrence_count > 1
|
|
23333
|
+
OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
|
|
23334
|
+
var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
|
|
23335
|
+
v.occurrence_count, v.first_seen, v.last_seen`;
|
|
23336
|
+
function toSighting(row) {
|
|
23337
|
+
return {
|
|
23338
|
+
location: row.location,
|
|
23339
|
+
kind: row.kind,
|
|
23340
|
+
firstSeen: new Date(row.first_seen).toISOString(),
|
|
23341
|
+
lastSeen: new Date(row.last_seen).toISOString()
|
|
23342
|
+
};
|
|
23343
|
+
}
|
|
23344
|
+
var SELECT_COLUMNS = `
|
|
23345
|
+
pointer_id AS pointerId,
|
|
23346
|
+
value_fingerprint AS valueFingerprint,
|
|
23347
|
+
fingerprint_key_version AS fingerprintKeyVersion,
|
|
23348
|
+
key_version AS keyVersion,
|
|
23349
|
+
format_version AS formatVersion,
|
|
23350
|
+
category,
|
|
23351
|
+
rule_id AS ruleId,
|
|
23352
|
+
masked_match AS maskedMatch,
|
|
23353
|
+
provider,
|
|
23354
|
+
ciphertext,
|
|
23355
|
+
nonce,
|
|
23356
|
+
auth_tag AS authTag,
|
|
23357
|
+
occurrence_count AS occurrenceCount,
|
|
23358
|
+
first_seen AS firstSeen,
|
|
23359
|
+
last_seen AS lastSeen`;
|
|
23360
|
+
function toRow(raw) {
|
|
23361
|
+
const { provider, ...rest } = raw;
|
|
23362
|
+
return provider === null ? rest : { ...rest, provider };
|
|
23363
|
+
}
|
|
23364
|
+
var SqliteSecretVaultRepository = class {
|
|
23365
|
+
constructor(db) {
|
|
22268
23366
|
this.db = db;
|
|
22269
|
-
this.
|
|
23367
|
+
this.insertStmt = db.prepare(
|
|
23368
|
+
`INSERT INTO secret_vault (
|
|
23369
|
+
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
23370
|
+
format_version, category, rule_id, masked_match, provider,
|
|
23371
|
+
ciphertext, nonce, auth_tag,
|
|
23372
|
+
occurrence_count, first_seen, last_seen
|
|
23373
|
+
) VALUES (
|
|
23374
|
+
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
23375
|
+
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
23376
|
+
:ciphertext, :nonce, :authTag,
|
|
23377
|
+
1, :now, :now
|
|
23378
|
+
)`
|
|
23379
|
+
);
|
|
23380
|
+
this.bumpStmt = db.prepare(
|
|
23381
|
+
`UPDATE secret_vault
|
|
23382
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
23383
|
+
WHERE value_fingerprint = :valueFingerprint`
|
|
23384
|
+
);
|
|
23385
|
+
this.byPointerStmt = db.prepare(
|
|
23386
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
|
|
23387
|
+
);
|
|
23388
|
+
this.byFingerprintStmt = db.prepare(
|
|
23389
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
|
|
23390
|
+
);
|
|
23391
|
+
this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
|
|
23392
|
+
this.replaceCiphertextStmt = db.prepare(
|
|
23393
|
+
`UPDATE secret_vault
|
|
23394
|
+
SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
|
|
23395
|
+
WHERE pointer_id = :pointerId`
|
|
23396
|
+
);
|
|
23397
|
+
this.refreshFingerprintStmt = db.prepare(
|
|
23398
|
+
`UPDATE secret_vault
|
|
23399
|
+
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
23400
|
+
WHERE pointer_id = :pointerId`
|
|
23401
|
+
);
|
|
23402
|
+
this.derefStmt = db.prepare(
|
|
23403
|
+
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
23404
|
+
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
23405
|
+
);
|
|
22270
23406
|
}
|
|
22271
23407
|
db;
|
|
22272
|
-
|
|
22273
|
-
|
|
22274
|
-
|
|
22275
|
-
|
|
22276
|
-
|
|
22277
|
-
|
|
22278
|
-
|
|
22279
|
-
|
|
22280
|
-
|
|
22281
|
-
|
|
22282
|
-
|
|
22283
|
-
|
|
22284
|
-
|
|
22285
|
-
|
|
22286
|
-
|
|
22287
|
-
|
|
22288
|
-
|
|
22289
|
-
|
|
22290
|
-
|
|
22291
|
-
|
|
22292
|
-
|
|
22293
|
-
|
|
22294
|
-
|
|
22295
|
-
|
|
22296
|
-
|
|
22297
|
-
|
|
22298
|
-
|
|
22299
|
-
|
|
22300
|
-
|
|
22301
|
-
|
|
22302
|
-
|
|
22303
|
-
|
|
22304
|
-
|
|
22305
|
-
|
|
22306
|
-
|
|
22307
|
-
|
|
22308
|
-
|
|
22309
|
-
|
|
22310
|
-
|
|
22311
|
-
|
|
22312
|
-
|
|
22313
|
-
|
|
22314
|
-
|
|
22315
|
-
|
|
22316
|
-
|
|
23408
|
+
insertStmt;
|
|
23409
|
+
bumpStmt;
|
|
23410
|
+
byPointerStmt;
|
|
23411
|
+
byFingerprintStmt;
|
|
23412
|
+
listStmt;
|
|
23413
|
+
replaceCiphertextStmt;
|
|
23414
|
+
refreshFingerprintStmt;
|
|
23415
|
+
derefStmt;
|
|
23416
|
+
/**
|
|
23417
|
+
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
23418
|
+
* `valueFingerprint`, never on the caller's pointer id: a value seen again
|
|
23419
|
+
* bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
|
|
23420
|
+
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
23421
|
+
* wire token. `minted` is true only when this call created the row.
|
|
23422
|
+
*
|
|
23423
|
+
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
23424
|
+
* writers cannot both decide they are minting.
|
|
23425
|
+
*/
|
|
23426
|
+
upsert(input, now) {
|
|
23427
|
+
let minted = false;
|
|
23428
|
+
withTransaction(
|
|
23429
|
+
this.db,
|
|
23430
|
+
() => {
|
|
23431
|
+
const existing = getRow(this.byFingerprintStmt, {
|
|
23432
|
+
valueFingerprint: input.valueFingerprint
|
|
23433
|
+
});
|
|
23434
|
+
if (existing === void 0) {
|
|
23435
|
+
this.insertStmt.run(
|
|
23436
|
+
bindParams({
|
|
23437
|
+
pointerId: input.pointerId,
|
|
23438
|
+
valueFingerprint: input.valueFingerprint,
|
|
23439
|
+
fingerprintKeyVersion: input.fingerprintKeyVersion,
|
|
23440
|
+
keyVersion: input.keyVersion,
|
|
23441
|
+
formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
|
|
23442
|
+
category: input.category,
|
|
23443
|
+
ruleId: input.ruleId,
|
|
23444
|
+
maskedMatch: input.maskedMatch,
|
|
23445
|
+
provider: input.provider,
|
|
23446
|
+
ciphertext: input.ciphertext,
|
|
23447
|
+
nonce: input.nonce,
|
|
23448
|
+
authTag: input.authTag,
|
|
23449
|
+
now
|
|
23450
|
+
})
|
|
23451
|
+
);
|
|
23452
|
+
minted = true;
|
|
23453
|
+
return;
|
|
23454
|
+
}
|
|
23455
|
+
this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
|
|
23456
|
+
},
|
|
23457
|
+
"IMMEDIATE"
|
|
23458
|
+
);
|
|
23459
|
+
const row = getRow(this.byFingerprintStmt, {
|
|
23460
|
+
valueFingerprint: input.valueFingerprint
|
|
23461
|
+
});
|
|
23462
|
+
if (row === void 0) throw new Error("vault: row vanished immediately after write");
|
|
23463
|
+
return { row: toRow(row), minted };
|
|
23464
|
+
}
|
|
23465
|
+
byPointerId(pointerId) {
|
|
23466
|
+
const raw = getRow(this.byPointerStmt, { pointerId });
|
|
23467
|
+
return raw === void 0 ? null : toRow(raw);
|
|
23468
|
+
}
|
|
23469
|
+
byValueFingerprint(fingerprint) {
|
|
23470
|
+
const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
|
|
23471
|
+
return raw === void 0 ? null : toRow(raw);
|
|
23472
|
+
}
|
|
23473
|
+
/** Append one audit row. Carries no raw value and no ciphertext, by shape. */
|
|
23474
|
+
recordDeref(entry) {
|
|
23475
|
+
this.derefStmt.run(
|
|
23476
|
+
bindParams({
|
|
23477
|
+
id: entry.id,
|
|
23478
|
+
pointerId: entry.pointerId,
|
|
23479
|
+
at: entry.at,
|
|
23480
|
+
target: entry.target,
|
|
23481
|
+
reason: entry.reason,
|
|
23482
|
+
outcome: entry.outcome,
|
|
23483
|
+
grantId: entry.grantId,
|
|
23484
|
+
pointerCount: entry.pointerCount ?? 1
|
|
23485
|
+
})
|
|
23486
|
+
);
|
|
23487
|
+
}
|
|
23488
|
+
listAll() {
|
|
23489
|
+
return allRows(this.listStmt).map(toRow);
|
|
23490
|
+
}
|
|
23491
|
+
/** Re-seal an entry under a new key epoch, leaving its identity untouched. */
|
|
23492
|
+
replaceCiphertext(pointerId, next) {
|
|
23493
|
+
this.replaceCiphertextStmt.run({ pointerId, ...next });
|
|
23494
|
+
}
|
|
23495
|
+
/** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
|
|
23496
|
+
refreshFingerprint(pointerId, next) {
|
|
23497
|
+
this.refreshFingerprintStmt.run({ pointerId, ...next });
|
|
23498
|
+
}
|
|
23499
|
+
/**
|
|
23500
|
+
* Destroy every vaulted value and report how many were destroyed. The deref
|
|
23501
|
+
* audit is left alone on purpose — see the table note above.
|
|
23502
|
+
*/
|
|
23503
|
+
purgeAll() {
|
|
23504
|
+
let destroyed = 0;
|
|
23505
|
+
withTransaction(
|
|
23506
|
+
this.db,
|
|
23507
|
+
() => {
|
|
23508
|
+
destroyed = this.countEntries();
|
|
23509
|
+
this.db.exec("DELETE FROM secret_vault");
|
|
23510
|
+
},
|
|
23511
|
+
"IMMEDIATE"
|
|
23512
|
+
);
|
|
23513
|
+
return destroyed;
|
|
23514
|
+
}
|
|
23515
|
+
/**
|
|
23516
|
+
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
23517
|
+
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
23518
|
+
* on hook paths — a failure must never affect the rewrite that triggered it,
|
|
23519
|
+
* so callers wrap this, not the other way around.
|
|
23520
|
+
*/
|
|
23521
|
+
recordSighting(entry, now) {
|
|
23522
|
+
this.db.prepare(
|
|
23523
|
+
`INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
|
|
23524
|
+
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
23525
|
+
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
23526
|
+
).run({
|
|
23527
|
+
id: randomUUID8(),
|
|
23528
|
+
pointerId: entry.pointerId,
|
|
23529
|
+
location: entry.location,
|
|
23530
|
+
kind: entry.kind,
|
|
23531
|
+
now
|
|
23532
|
+
});
|
|
23533
|
+
}
|
|
23534
|
+
/**
|
|
23535
|
+
* Sightings for a whole page of pointers, in ONE query grouped in JS rather
|
|
23536
|
+
* than one query per row. A pointer with no sightings still gets an entry, so
|
|
23537
|
+
* the caller never has to distinguish "none" from "missing".
|
|
23538
|
+
*
|
|
23539
|
+
* The `IN` list is sized to the page, so this statement cannot be cached on
|
|
23540
|
+
* the instance the way the fixed-shape ones in the constructor are.
|
|
23541
|
+
*/
|
|
23542
|
+
sightingsFor(pointerIds) {
|
|
23543
|
+
const byPointer = new Map(pointerIds.map((id) => [id, []]));
|
|
23544
|
+
if (pointerIds.length === 0) return byPointer;
|
|
23545
|
+
const rows = allRows(
|
|
23546
|
+
this.db.prepare(
|
|
23547
|
+
`SELECT pointer_id, location, kind, first_seen, last_seen
|
|
23548
|
+
FROM secret_vault_sighting
|
|
23549
|
+
WHERE pointer_id IN (${placeholders(pointerIds.length)})
|
|
23550
|
+
ORDER BY last_seen DESC`
|
|
23551
|
+
),
|
|
23552
|
+
pointerIds
|
|
23553
|
+
);
|
|
23554
|
+
for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
|
|
23555
|
+
return byPointer;
|
|
23556
|
+
}
|
|
23557
|
+
/** Hydrate a page of raw inventory rows with their sightings, batched. */
|
|
23558
|
+
toInventoryEntries(rows) {
|
|
23559
|
+
const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
|
|
23560
|
+
return rows.map((r) => ({
|
|
23561
|
+
pointerId: r.pointer_id,
|
|
23562
|
+
category: r.category,
|
|
23563
|
+
...r.provider === null ? {} : { provider: r.provider },
|
|
23564
|
+
maskedMatch: r.masked_match,
|
|
23565
|
+
occurrences: r.occurrence_count,
|
|
23566
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
23567
|
+
lastSeen: new Date(r.last_seen).toISOString(),
|
|
23568
|
+
revealGrantId: r.grant_id,
|
|
23569
|
+
sightings: sightings.get(r.pointer_id) ?? []
|
|
23570
|
+
}));
|
|
23571
|
+
}
|
|
23572
|
+
/**
|
|
23573
|
+
* The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
|
|
23574
|
+
* value's descriptor data joined with its sightings and the active
|
|
23575
|
+
* reveal-to-model grant when one exists. Raw-free by construction — neither
|
|
23576
|
+
* the fingerprint nor the ciphertext columns are selected.
|
|
23577
|
+
*
|
|
23578
|
+
* `totals.values` counts the whole store, not the page, so the count a reader
|
|
23579
|
+
* sees never depends on how far they have paged.
|
|
23580
|
+
*/
|
|
23581
|
+
listInventory(query = {}, now = Date.now()) {
|
|
23582
|
+
const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
|
|
23583
|
+
const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
|
|
23584
|
+
const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
|
|
23585
|
+
const rows = allRows(
|
|
23586
|
+
this.db.prepare(
|
|
23587
|
+
`SELECT ${INVENTORY_COLUMNS},
|
|
23588
|
+
(SELECT e.id FROM exceptions e
|
|
23589
|
+
WHERE e.rule_id = v.rule_id
|
|
23590
|
+
AND e.value_fingerprint = v.value_fingerprint
|
|
23591
|
+
AND e.key_version = v.fingerprint_key_version
|
|
23592
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
23593
|
+
LIMIT 1) AS grant_id
|
|
23594
|
+
FROM secret_vault v
|
|
23595
|
+
${where}
|
|
23596
|
+
ORDER BY v.last_seen DESC, v.pointer_id DESC
|
|
23597
|
+
LIMIT :limit`
|
|
23598
|
+
),
|
|
23599
|
+
bindParams({
|
|
23600
|
+
now,
|
|
23601
|
+
limit: limit + 1,
|
|
23602
|
+
...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
|
|
23603
|
+
})
|
|
23604
|
+
);
|
|
23605
|
+
const hasMore = rows.length > limit;
|
|
23606
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
23607
|
+
const last = page[page.length - 1];
|
|
23608
|
+
return {
|
|
23609
|
+
totals: { values: this.countEntries() },
|
|
23610
|
+
items: this.toInventoryEntries(page),
|
|
23611
|
+
// Minted from the last row of the PAGE, never the extra probe row.
|
|
23612
|
+
nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
|
|
23613
|
+
};
|
|
23614
|
+
}
|
|
23615
|
+
/**
|
|
23616
|
+
* Values reused on this machine — detected more than once, or written to more
|
|
23617
|
+
* than one location — most-reused first, one page at a time.
|
|
23618
|
+
*
|
|
23619
|
+
* Its own read rather than a filter over an inventory page: reuse is a
|
|
23620
|
+
* property of the whole store, and deriving it from 50 newest rows would
|
|
23621
|
+
* under-report exactly the values a reader most needs to see.
|
|
23622
|
+
*/
|
|
23623
|
+
listReuse(query = {}, now = Date.now()) {
|
|
23624
|
+
const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
|
|
23625
|
+
const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
|
|
23626
|
+
const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
|
|
23627
|
+
OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
|
|
23628
|
+
const rows = allRows(
|
|
23629
|
+
this.db.prepare(
|
|
23630
|
+
`SELECT ${INVENTORY_COLUMNS},
|
|
23631
|
+
(SELECT e.id FROM exceptions e
|
|
23632
|
+
WHERE e.rule_id = v.rule_id
|
|
23633
|
+
AND e.value_fingerprint = v.value_fingerprint
|
|
23634
|
+
AND e.key_version = v.fingerprint_key_version
|
|
23635
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
23636
|
+
LIMIT 1) AS grant_id
|
|
23637
|
+
FROM secret_vault v
|
|
23638
|
+
WHERE ${REUSED_PREDICATE} ${after}
|
|
23639
|
+
ORDER BY v.occurrence_count DESC, v.pointer_id DESC
|
|
23640
|
+
LIMIT :limit`
|
|
23641
|
+
),
|
|
23642
|
+
bindParams({
|
|
23643
|
+
now,
|
|
23644
|
+
limit: limit + 1,
|
|
23645
|
+
...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
|
|
23646
|
+
})
|
|
23647
|
+
);
|
|
23648
|
+
const hasMore = rows.length > limit;
|
|
23649
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
23650
|
+
const last = page[page.length - 1];
|
|
23651
|
+
return {
|
|
23652
|
+
totals: { reused: this.countReused() },
|
|
23653
|
+
items: this.toInventoryEntries(page),
|
|
23654
|
+
nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
|
|
23655
|
+
};
|
|
23656
|
+
}
|
|
23657
|
+
/**
|
|
23658
|
+
* The de-reference trail, newest first, one page at a time. By default the
|
|
23659
|
+
* batched, high-volume reasons (display, view-render) are hidden and counted
|
|
23660
|
+
* instead — the rows that matter as a signal are the model crossings, and
|
|
23661
|
+
* burying them under render noise would defeat the audit's purpose.
|
|
23662
|
+
*
|
|
23663
|
+
* `hiddenBatched` counts the whole trail rather than the page: it is what the
|
|
23664
|
+
* view's "N hidden" line and its toggle speak for, so it must not shrink as
|
|
23665
|
+
* the reader pages.
|
|
23666
|
+
*/
|
|
23667
|
+
listDerefs(query = {}) {
|
|
23668
|
+
const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
|
|
23669
|
+
const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
|
|
23670
|
+
const conditions = [];
|
|
23671
|
+
if (query.includeBatched !== true) {
|
|
23672
|
+
conditions.push(`reason NOT IN ('display', 'view-render')`);
|
|
23673
|
+
}
|
|
23674
|
+
if (cursor !== null) {
|
|
23675
|
+
conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
|
|
23676
|
+
}
|
|
23677
|
+
const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
|
|
23678
|
+
const rows = allRows(
|
|
23679
|
+
this.db.prepare(
|
|
23680
|
+
`SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
|
|
23681
|
+
FROM secret_vault_deref ${where}
|
|
23682
|
+
ORDER BY at DESC, id DESC LIMIT :limit`
|
|
23683
|
+
),
|
|
23684
|
+
bindParams({
|
|
23685
|
+
limit: limit + 1,
|
|
23686
|
+
...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
|
|
23687
|
+
})
|
|
23688
|
+
);
|
|
23689
|
+
const hasMore = rows.length > limit;
|
|
23690
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
23691
|
+
const last = page[page.length - 1];
|
|
23692
|
+
const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
|
|
23693
|
+
this.db,
|
|
23694
|
+
`SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
|
|
23695
|
+
);
|
|
23696
|
+
return {
|
|
23697
|
+
items: page.map((r) => ({
|
|
23698
|
+
id: r.id,
|
|
23699
|
+
pointerId: r.pointer_id,
|
|
23700
|
+
at: new Date(r.at).toISOString(),
|
|
23701
|
+
target: r.target,
|
|
23702
|
+
reason: r.reason,
|
|
23703
|
+
outcome: r.outcome,
|
|
23704
|
+
...r.grant_id === null ? {} : { grantId: r.grant_id },
|
|
23705
|
+
pointerCount: r.pointer_count
|
|
23706
|
+
})),
|
|
23707
|
+
nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
|
|
23708
|
+
hiddenBatched
|
|
23709
|
+
};
|
|
23710
|
+
}
|
|
23711
|
+
countEntries() {
|
|
23712
|
+
return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
|
|
23713
|
+
}
|
|
23714
|
+
/** Values reused on this machine — the reuse list's page-independent total. */
|
|
23715
|
+
countReused() {
|
|
23716
|
+
return countScalar(
|
|
23717
|
+
this.db,
|
|
23718
|
+
`SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
|
|
23719
|
+
);
|
|
23720
|
+
}
|
|
23721
|
+
};
|
|
23722
|
+
|
|
23723
|
+
// ../../packages/persistence/src/repositories/security.ts
|
|
23724
|
+
var DAY_MS4 = 864e5;
|
|
23725
|
+
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
23726
|
+
var ACTION_TO_KIND = {
|
|
23727
|
+
block: "blocked",
|
|
23728
|
+
redact: "redacted",
|
|
23729
|
+
warn: "warned"
|
|
23730
|
+
};
|
|
23731
|
+
var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
|
|
23732
|
+
var SCAN_COVERAGE = [
|
|
23733
|
+
{ provider: "claudecode", coverage: 100, supported: true },
|
|
23734
|
+
{ provider: "cursor", coverage: 0, supported: false },
|
|
23735
|
+
{ provider: "codex", coverage: 80, supported: true },
|
|
23736
|
+
{ provider: "antigravity", coverage: 60, supported: true },
|
|
23737
|
+
{ provider: "claudeai", coverage: 0, supported: false },
|
|
23738
|
+
{ provider: "chatgpt", coverage: 0, supported: false },
|
|
23739
|
+
{ provider: "copilot", coverage: 0, supported: false },
|
|
23740
|
+
{ provider: "api", coverage: 0, supported: false }
|
|
23741
|
+
];
|
|
23742
|
+
var GRANULARITY = {
|
|
23743
|
+
"7d": "day",
|
|
23744
|
+
"30d": "day",
|
|
23745
|
+
"3m": "week",
|
|
23746
|
+
"6m": "week"
|
|
23747
|
+
};
|
|
23748
|
+
function granularityFor(range) {
|
|
23749
|
+
return GRANULARITY[range];
|
|
23750
|
+
}
|
|
23751
|
+
function startOfUtcDay2(ms) {
|
|
23752
|
+
return Math.floor(ms / DAY_MS4) * DAY_MS4;
|
|
23753
|
+
}
|
|
23754
|
+
function toUtcDateString(ms) {
|
|
23755
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
23756
|
+
}
|
|
23757
|
+
function isTimeseriesSeverity(s) {
|
|
23758
|
+
return s === "critical" || s === "high" || s === "medium";
|
|
23759
|
+
}
|
|
23760
|
+
var SqliteSecurityRepository = class {
|
|
23761
|
+
constructor(db, now = () => Date.now()) {
|
|
23762
|
+
this.db = db;
|
|
23763
|
+
this.now = now;
|
|
23764
|
+
}
|
|
23765
|
+
db;
|
|
23766
|
+
now;
|
|
23767
|
+
// Status-aware: every finding is classified by origin (its parent event's
|
|
23768
|
+
// kind — 'code_change' is at-rest, everything else is in-flight) and, for
|
|
23769
|
+
// at-rest findings, whether its finding_key's LATEST finding_resolution row
|
|
23770
|
+
// (max created_at, not "does ANY row exist") has status 'resolved' — mirrors
|
|
23771
|
+
// SqliteResolutionsRepository's LATEST-RESOLUTION-WINS convention. "Any row
|
|
23772
|
+
// exists" would let a fixed-at-source key that is later redetected (the same
|
|
23773
|
+
// secret re-added) stay silently "caught" forever under its stale resolved
|
|
23774
|
+
// row; latest-wins lets the scanner supersede it with a fresh status:'open'
|
|
23775
|
+
// row (see scan.ts's reopenRedetectedFindings) so the invariant holds: a
|
|
23776
|
+
// finding_key present in the current scan is OPEN, regardless of history.
|
|
23777
|
+
// In-flight findings are born caught (enforcement already ran); at-rest
|
|
23778
|
+
// findings are caught only once their latest disposition is resolved,
|
|
23779
|
+
// otherwise they are open-at-rest.
|
|
23780
|
+
//
|
|
23781
|
+
// NOTE for future manual-resolution writers: only latest status
|
|
23782
|
+
// 'resolved' counts as caught above. When acknowledged/dismissed/
|
|
23783
|
+
// false-positive manual dispositions land, this must keep filtering by
|
|
23784
|
+
// status/method — 'acknowledged' is accepted risk, not a fix, and must NOT
|
|
23785
|
+
// be bucketed as caught alongside 'resolved'.
|
|
23786
|
+
//
|
|
23787
|
+
// Legacy at-rest findings from pre-branch scans carry finding_key = NULL —
|
|
23788
|
+
// the resolution lifecycle is keyed by finding_key, so it can never attach a
|
|
23789
|
+
// disposition to (or clear) one of these on re-scan. They are excluded from
|
|
23790
|
+
// both caught and openAtRest (untracked, not "needs remediation forever"),
|
|
23791
|
+
// but still counted in total/count below — this keeps this predicate
|
|
23792
|
+
// consistent with SqliteResolutionsRepository.openAtRestKeysForPath, which
|
|
23793
|
+
// already filters `finding_key IS NOT NULL`.
|
|
23794
|
+
//
|
|
23795
|
+
// One GROUP BY aggregate: the result set stays O(distinct severities) no
|
|
23796
|
+
// matter how many findings the store has accumulated (this backs `aka stats`
|
|
23797
|
+
// and the dashboard severity card, both hot paths on a table that only
|
|
23798
|
+
// grows). The latest-resolution status comes from the shared derived-table
|
|
23799
|
+
// fragment (see resolution-sql.ts) rather than a correlated subquery per
|
|
23800
|
+
// finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
|
|
23801
|
+
// double-counting a key that accumulated several append-only rows.
|
|
23802
|
+
severitySummary() {
|
|
23803
|
+
const rows = allRows(
|
|
23804
|
+
this.db.prepare(
|
|
23805
|
+
`SELECT d.severity AS severity,
|
|
23806
|
+
COUNT(*) AS count,
|
|
23807
|
+
SUM(CASE
|
|
23808
|
+
WHEN e.event_type != 'code_change' THEN 1
|
|
23809
|
+
WHEN f.finding_key IS NULL THEN 0
|
|
23810
|
+
WHEN latest.status = 'resolved' THEN 1
|
|
22317
23811
|
ELSE 0
|
|
22318
23812
|
END) AS caught,
|
|
22319
23813
|
SUM(CASE
|
|
@@ -22573,7 +24067,7 @@ var SqliteSecurityRepository = class {
|
|
|
22573
24067
|
};
|
|
22574
24068
|
|
|
22575
24069
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22576
|
-
import { randomUUID as
|
|
24070
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
22577
24071
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22578
24072
|
var IN_CHUNK = 500;
|
|
22579
24073
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -22829,7 +24323,7 @@ var SqliteSharesRepository = class {
|
|
|
22829
24323
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
22830
24324
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22831
24325
|
).run({
|
|
22832
|
-
id:
|
|
24326
|
+
id: randomUUID9(),
|
|
22833
24327
|
destinationId,
|
|
22834
24328
|
host: dest.host,
|
|
22835
24329
|
decision,
|
|
@@ -22978,7 +24472,7 @@ var SqliteSharesRepository = class {
|
|
|
22978
24472
|
let destinationId = destIds.get(hit.host);
|
|
22979
24473
|
if (destinationId === void 0) {
|
|
22980
24474
|
destStmt.run({
|
|
22981
|
-
id:
|
|
24475
|
+
id: randomUUID9(),
|
|
22982
24476
|
kind: hit.kind,
|
|
22983
24477
|
name: hit.name,
|
|
22984
24478
|
host: hit.host,
|
|
@@ -22994,7 +24488,7 @@ var SqliteSharesRepository = class {
|
|
|
22994
24488
|
let endpointId = endpointIds.get(endpointKey);
|
|
22995
24489
|
if (endpointId === void 0) {
|
|
22996
24490
|
endpointStmt.run({
|
|
22997
|
-
id:
|
|
24491
|
+
id: randomUUID9(),
|
|
22998
24492
|
destinationId,
|
|
22999
24493
|
method: hit.method,
|
|
23000
24494
|
transport: hit.transport,
|
|
@@ -23007,7 +24501,7 @@ var SqliteSharesRepository = class {
|
|
|
23007
24501
|
endpointIds.set(endpointKey, endpointId);
|
|
23008
24502
|
}
|
|
23009
24503
|
siteStmt.run({
|
|
23010
|
-
id:
|
|
24504
|
+
id: randomUUID9(),
|
|
23011
24505
|
endpointId,
|
|
23012
24506
|
project: input.project,
|
|
23013
24507
|
projectKey: input.projectKey,
|
|
@@ -23372,6 +24866,9 @@ function purgeSampleData(db) {
|
|
|
23372
24866
|
}
|
|
23373
24867
|
|
|
23374
24868
|
// ../../packages/persistence/src/database.ts
|
|
24869
|
+
var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
|
|
24870
|
+
"aka.persistence.unsafeTestOnlyRawHandle"
|
|
24871
|
+
);
|
|
23375
24872
|
function linkHost(input, hostId) {
|
|
23376
24873
|
return hostId ? { ...input, hostId } : input;
|
|
23377
24874
|
}
|
|
@@ -23393,21 +24890,34 @@ function openWithPragmas(file2) {
|
|
|
23393
24890
|
}
|
|
23394
24891
|
return db;
|
|
23395
24892
|
}
|
|
23396
|
-
function backupLegacyStore(file2) {
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
24893
|
+
function backupLegacyStore(db, file2) {
|
|
24894
|
+
reapStalePartials(file2);
|
|
24895
|
+
const backup = backupPath(file2, "legacy");
|
|
24896
|
+
let snapshotted = false;
|
|
24897
|
+
let snapshotError;
|
|
24898
|
+
try {
|
|
24899
|
+
snapshotStore(db, backup);
|
|
24900
|
+
snapshotted = true;
|
|
24901
|
+
} catch (error51) {
|
|
24902
|
+
snapshotError = error51;
|
|
24903
|
+
} finally {
|
|
24904
|
+
db.close();
|
|
23402
24905
|
}
|
|
24906
|
+
if (!snapshotted) {
|
|
24907
|
+
akaWarn(
|
|
24908
|
+
`Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
|
|
24909
|
+
);
|
|
24910
|
+
moveStoreAside(file2, backup);
|
|
24911
|
+
return backup;
|
|
24912
|
+
}
|
|
24913
|
+
discardStore(file2, backup);
|
|
23403
24914
|
return backup;
|
|
23404
24915
|
}
|
|
23405
24916
|
function openAndInitialize(file2) {
|
|
23406
24917
|
let db = openWithPragmas(file2);
|
|
23407
24918
|
try {
|
|
23408
24919
|
if (isForeignSqliteLineage(db)) {
|
|
23409
|
-
db
|
|
23410
|
-
const backup = backupLegacyStore(file2);
|
|
24920
|
+
const backup = backupLegacyStore(db, file2);
|
|
23411
24921
|
db = openWithPragmas(file2);
|
|
23412
24922
|
akaWarn(
|
|
23413
24923
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
@@ -23423,6 +24933,7 @@ function openAndInitialize(file2) {
|
|
|
23423
24933
|
policies,
|
|
23424
24934
|
installedPacks,
|
|
23425
24935
|
scanLedger: new SqliteScanLedgerRepository(db),
|
|
24936
|
+
secretVault: new SqliteSecretVaultRepository(db),
|
|
23426
24937
|
exceptions: new SqliteExceptionsRepository(db),
|
|
23427
24938
|
resolutions: new SqliteResolutionsRepository(db),
|
|
23428
24939
|
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
@@ -23450,7 +24961,7 @@ function openAndInitialize(file2) {
|
|
|
23450
24961
|
}
|
|
23451
24962
|
function openLocalDatabase(dir) {
|
|
23452
24963
|
ensureDataDirSync(dir);
|
|
23453
|
-
const file2 =
|
|
24964
|
+
const file2 = join2(dir, DB_FILENAME);
|
|
23454
24965
|
const {
|
|
23455
24966
|
db,
|
|
23456
24967
|
events,
|
|
@@ -23458,6 +24969,7 @@ function openLocalDatabase(dir) {
|
|
|
23458
24969
|
policies,
|
|
23459
24970
|
installedPacks,
|
|
23460
24971
|
scanLedger,
|
|
24972
|
+
secretVault,
|
|
23461
24973
|
exceptions,
|
|
23462
24974
|
resolutions,
|
|
23463
24975
|
ruleProbeCache,
|
|
@@ -23566,7 +25078,7 @@ function openLocalDatabase(dir) {
|
|
|
23566
25078
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
23567
25079
|
if (!definitionId) continue;
|
|
23568
25080
|
inspectionFindings.insertFinding({
|
|
23569
|
-
id:
|
|
25081
|
+
id: randomUUID10(),
|
|
23570
25082
|
auditEventId: record2.scanEvent.id,
|
|
23571
25083
|
inspectionDefinitionId: definitionId,
|
|
23572
25084
|
span: finding.span,
|
|
@@ -23643,6 +25155,7 @@ function openLocalDatabase(dir) {
|
|
|
23643
25155
|
policies,
|
|
23644
25156
|
installedPacks,
|
|
23645
25157
|
scanLedger,
|
|
25158
|
+
secretVault,
|
|
23646
25159
|
exceptions,
|
|
23647
25160
|
resolutions,
|
|
23648
25161
|
ruleProbeCache,
|
|
@@ -23671,22 +25184,58 @@ function openLocalDatabase(dir) {
|
|
|
23671
25184
|
transaction,
|
|
23672
25185
|
close: () => {
|
|
23673
25186
|
db.close();
|
|
23674
|
-
}
|
|
25187
|
+
},
|
|
25188
|
+
// Last, and a plain value rather than a getter, so `{ ...db }` carries it.
|
|
25189
|
+
[UNSAFE_TEST_ONLY_RAW_HANDLE]: db
|
|
23675
25190
|
};
|
|
23676
25191
|
}
|
|
23677
25192
|
|
|
25193
|
+
// ../../packages/persistence/src/exception-policy.ts
|
|
25194
|
+
var UserGrantPolicyProvider = class {
|
|
25195
|
+
#exceptions;
|
|
25196
|
+
constructor(exceptions) {
|
|
25197
|
+
this.#exceptions = exceptions;
|
|
25198
|
+
}
|
|
25199
|
+
async decideReveal(identity) {
|
|
25200
|
+
try {
|
|
25201
|
+
const grant = await this.#exceptions.activeRevealGrant(
|
|
25202
|
+
identity.ruleId,
|
|
25203
|
+
identity.valueFingerprint,
|
|
25204
|
+
identity.fingerprintKeyVersion
|
|
25205
|
+
);
|
|
25206
|
+
return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
|
|
25207
|
+
} catch {
|
|
25208
|
+
return { allow: false };
|
|
25209
|
+
}
|
|
25210
|
+
}
|
|
25211
|
+
};
|
|
25212
|
+
|
|
25213
|
+
// ../../packages/persistence/src/file-lock.ts
|
|
25214
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
25215
|
+
import {
|
|
25216
|
+
closeSync,
|
|
25217
|
+
existsSync as existsSync2,
|
|
25218
|
+
openSync,
|
|
25219
|
+
readFileSync,
|
|
25220
|
+
rmSync as rmSync3,
|
|
25221
|
+
statSync as statSync2,
|
|
25222
|
+
writeFileSync as writeFileSync2
|
|
25223
|
+
} from "fs";
|
|
25224
|
+
import { hostname as hostname3 } from "os";
|
|
25225
|
+
var PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
25226
|
+
|
|
23678
25227
|
// ../../packages/persistence/src/finding-key.ts
|
|
23679
25228
|
import { createHash as createHash3 } from "crypto";
|
|
23680
25229
|
|
|
23681
25230
|
// ../../packages/persistence/src/fingerprint.ts
|
|
23682
25231
|
import { createHmac, randomBytes } from "crypto";
|
|
23683
|
-
import { existsSync as
|
|
23684
|
-
import { join as
|
|
25232
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
25233
|
+
import { join as join3 } from "path";
|
|
23685
25234
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
23686
|
-
var
|
|
25235
|
+
var EXCEPTION_KEY_FILENAME = "exception.key";
|
|
23687
25236
|
var KEY_MATERIAL_BYTES = 32;
|
|
23688
25237
|
function keyFilePath(dataDir2) {
|
|
23689
|
-
return
|
|
25238
|
+
return join3(dataDir2, EXCEPTION_KEY_FILENAME);
|
|
23690
25239
|
}
|
|
23691
25240
|
function parseKeyFile(raw) {
|
|
23692
25241
|
const parsed = JSON.parse(raw);
|
|
@@ -23706,33 +25255,120 @@ function parseKeyFile(raw) {
|
|
|
23706
25255
|
}
|
|
23707
25256
|
return { version: version2, material: bytes };
|
|
23708
25257
|
}
|
|
25258
|
+
var KEY_VERSION_COLUMNS = {
|
|
25259
|
+
exceptions: "key_version",
|
|
25260
|
+
blocked_detections: "key_version",
|
|
25261
|
+
secret_vault: "fingerprint_key_version"
|
|
25262
|
+
};
|
|
25263
|
+
var SQLITE_ERROR = 1;
|
|
25264
|
+
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
25265
|
+
var FloorUnreadableError = class extends Error {
|
|
25266
|
+
code = "floor-unreadable";
|
|
25267
|
+
constructor(cause) {
|
|
25268
|
+
super(
|
|
25269
|
+
`cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
25270
|
+
{ cause }
|
|
25271
|
+
);
|
|
25272
|
+
this.name = "FloorUnreadableError";
|
|
25273
|
+
}
|
|
25274
|
+
};
|
|
25275
|
+
function storedKeyVersionFloor(dataDir2) {
|
|
25276
|
+
const file2 = join3(dataDir2, DB_FILENAME);
|
|
25277
|
+
if (!existsSync3(file2)) return 0;
|
|
25278
|
+
let db;
|
|
25279
|
+
try {
|
|
25280
|
+
db = new DatabaseSync2(file2, { readOnly: true });
|
|
25281
|
+
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
25282
|
+
let floor = 0;
|
|
25283
|
+
for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
|
|
25284
|
+
try {
|
|
25285
|
+
const row = getRow(
|
|
25286
|
+
db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
|
|
25287
|
+
);
|
|
25288
|
+
floor = Math.max(floor, row?.v ?? 0);
|
|
25289
|
+
} catch (err) {
|
|
25290
|
+
if (err.errcode !== SQLITE_ERROR) {
|
|
25291
|
+
throw new FloorUnreadableError(err);
|
|
25292
|
+
}
|
|
25293
|
+
}
|
|
25294
|
+
}
|
|
25295
|
+
return floor;
|
|
25296
|
+
} catch (err) {
|
|
25297
|
+
throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
|
|
25298
|
+
} finally {
|
|
25299
|
+
db?.close();
|
|
25300
|
+
}
|
|
25301
|
+
}
|
|
25302
|
+
function serializeKey(key) {
|
|
25303
|
+
return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
|
|
25304
|
+
}
|
|
25305
|
+
function createKeyFile(dataDir2, key) {
|
|
25306
|
+
ensureDataDirSync(dataDir2);
|
|
25307
|
+
const file2 = keyFilePath(dataDir2);
|
|
25308
|
+
if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
|
|
25309
|
+
`)) return key;
|
|
25310
|
+
const winner = readFingerprintKey(dataDir2);
|
|
25311
|
+
if (winner) {
|
|
25312
|
+
tightenFile(file2);
|
|
25313
|
+
return winner;
|
|
25314
|
+
}
|
|
25315
|
+
const occupant = classifyOccupant(file2);
|
|
25316
|
+
throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
|
|
25317
|
+
}
|
|
25318
|
+
function occupantMessage(file2, kind) {
|
|
25319
|
+
switch (kind) {
|
|
25320
|
+
case "symlink":
|
|
25321
|
+
return `exception key file is a symlink (${file2}); remove it so a key can be created`;
|
|
25322
|
+
case "gone":
|
|
25323
|
+
return "exception key file was removed while it was being created";
|
|
25324
|
+
case "unknown":
|
|
25325
|
+
return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
|
|
25326
|
+
}
|
|
25327
|
+
}
|
|
23709
25328
|
function readFingerprintKey(dataDir2) {
|
|
23710
25329
|
let raw;
|
|
23711
25330
|
try {
|
|
23712
|
-
raw =
|
|
25331
|
+
raw = readFileSync2(keyFilePath(dataDir2), "utf8");
|
|
23713
25332
|
} catch (err) {
|
|
23714
25333
|
if (err.code === "ENOENT") return null;
|
|
23715
25334
|
throw err instanceof Error ? err : new Error(String(err));
|
|
23716
25335
|
}
|
|
23717
25336
|
return parseKeyFile(raw);
|
|
23718
25337
|
}
|
|
25338
|
+
function loadOrCreateFingerprintKey(dataDir2) {
|
|
25339
|
+
const existing = readFingerprintKey(dataDir2);
|
|
25340
|
+
if (existing) {
|
|
25341
|
+
tightenFile(keyFilePath(dataDir2));
|
|
25342
|
+
return existing;
|
|
25343
|
+
}
|
|
25344
|
+
return createKeyFile(dataDir2, {
|
|
25345
|
+
version: storedKeyVersionFloor(dataDir2) + 1,
|
|
25346
|
+
material: randomBytes(KEY_MATERIAL_BYTES)
|
|
25347
|
+
});
|
|
25348
|
+
}
|
|
25349
|
+
function fingerprintValue(key, raw) {
|
|
25350
|
+
return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
|
|
25351
|
+
}
|
|
23719
25352
|
|
|
23720
25353
|
// ../../packages/persistence/src/local-layout.ts
|
|
23721
25354
|
import { renameSync as renameSync3 } from "fs";
|
|
23722
25355
|
import { mkdir } from "fs/promises";
|
|
23723
25356
|
import { homedir } from "os";
|
|
23724
|
-
import { join as
|
|
25357
|
+
import { join as join4 } from "path";
|
|
23725
25358
|
function defaultDataDir() {
|
|
23726
|
-
return
|
|
25359
|
+
return join4(homedir(), ".aka");
|
|
23727
25360
|
}
|
|
23728
25361
|
function settingsDir(base = defaultDataDir()) {
|
|
23729
|
-
return
|
|
25362
|
+
return join4(base, "settings");
|
|
23730
25363
|
}
|
|
23731
25364
|
function dataDir(base = defaultDataDir()) {
|
|
23732
|
-
return
|
|
25365
|
+
return join4(base, "data");
|
|
23733
25366
|
}
|
|
23734
25367
|
function dbPath(base = defaultDataDir()) {
|
|
23735
|
-
return
|
|
25368
|
+
return join4(dataDir(base), "aka.db");
|
|
25369
|
+
}
|
|
25370
|
+
function keysDir(base = defaultDataDir()) {
|
|
25371
|
+
return join4(base, "keys");
|
|
23736
25372
|
}
|
|
23737
25373
|
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23738
25374
|
ensureDataDirSync(dir);
|
|
@@ -23745,8 +25381,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23745
25381
|
for (const { name, dest } of moves) {
|
|
23746
25382
|
try {
|
|
23747
25383
|
ensureDataDirSync(dest);
|
|
23748
|
-
const moved =
|
|
23749
|
-
renameSync3(
|
|
25384
|
+
const moved = join4(dest, name);
|
|
25385
|
+
renameSync3(join4(base, name), moved);
|
|
23750
25386
|
tightenFile(moved);
|
|
23751
25387
|
} catch {
|
|
23752
25388
|
}
|
|
@@ -23754,10 +25390,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
|
|
|
23754
25390
|
}
|
|
23755
25391
|
|
|
23756
25392
|
// ../../packages/persistence/src/settings.ts
|
|
23757
|
-
import { readFileSync as
|
|
23758
|
-
import { join as
|
|
25393
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
25394
|
+
import { join as join5 } from "path";
|
|
25395
|
+
var SETTINGS_FILENAME = "settings.json";
|
|
23759
25396
|
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
23760
|
-
const record2 = readJson(
|
|
25397
|
+
const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
|
|
23761
25398
|
if (!record2) return defaultWorkspaceSettings();
|
|
23762
25399
|
try {
|
|
23763
25400
|
return WorkspaceSettings.parse(record2);
|
|
@@ -23768,23 +25405,898 @@ function readWorkspaceSettings(base = defaultDataDir()) {
|
|
|
23768
25405
|
function readJson(file2) {
|
|
23769
25406
|
let text;
|
|
23770
25407
|
try {
|
|
23771
|
-
text =
|
|
25408
|
+
text = readFileSync3(file2, "utf8");
|
|
23772
25409
|
} catch {
|
|
23773
25410
|
return null;
|
|
23774
25411
|
}
|
|
23775
25412
|
return parseJsonObject(text) ?? null;
|
|
23776
25413
|
}
|
|
23777
25414
|
|
|
25415
|
+
// ../../packages/persistence/src/vault/crypto.ts
|
|
25416
|
+
import {
|
|
25417
|
+
createCipheriv,
|
|
25418
|
+
createDecipheriv,
|
|
25419
|
+
createHmac as createHmac2,
|
|
25420
|
+
hkdfSync,
|
|
25421
|
+
timingSafeEqual
|
|
25422
|
+
} from "crypto";
|
|
25423
|
+
var POINTER_ID_BYTES = 16;
|
|
25424
|
+
var NONCE_BYTES = 12;
|
|
25425
|
+
var TAG_BYTES = 10;
|
|
25426
|
+
var SUBKEY_BYTES = 32;
|
|
25427
|
+
var HKDF_INFO_ENC = "aka:vault:enc:v1";
|
|
25428
|
+
var HKDF_INFO_SIGN = "aka:vault:sign:v1";
|
|
25429
|
+
var HKDF_SALT = "aka:vault:v1";
|
|
25430
|
+
var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
25431
|
+
function base32Encode(bytes) {
|
|
25432
|
+
let out = "";
|
|
25433
|
+
let buffer = 0;
|
|
25434
|
+
let bits = 0;
|
|
25435
|
+
for (const byte of bytes) {
|
|
25436
|
+
buffer = buffer << 8 | byte;
|
|
25437
|
+
bits += 8;
|
|
25438
|
+
while (bits >= 5) {
|
|
25439
|
+
out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
|
|
25440
|
+
bits -= 5;
|
|
25441
|
+
}
|
|
25442
|
+
}
|
|
25443
|
+
if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
|
|
25444
|
+
return out;
|
|
25445
|
+
}
|
|
25446
|
+
function base32Decode(text) {
|
|
25447
|
+
const out = [];
|
|
25448
|
+
let buffer = 0;
|
|
25449
|
+
let bits = 0;
|
|
25450
|
+
for (const char of text) {
|
|
25451
|
+
const value = B32_ALPHABET.indexOf(char);
|
|
25452
|
+
if (value < 0) throw new Error("base32: character outside the alphabet");
|
|
25453
|
+
buffer = buffer << 5 | value;
|
|
25454
|
+
bits += 5;
|
|
25455
|
+
if (bits >= 8) {
|
|
25456
|
+
out.push(buffer >>> bits - 8 & 255);
|
|
25457
|
+
bits -= 8;
|
|
25458
|
+
}
|
|
25459
|
+
}
|
|
25460
|
+
return Buffer.from(out);
|
|
25461
|
+
}
|
|
25462
|
+
function encodeKeyVersion(version2) {
|
|
25463
|
+
if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
|
|
25464
|
+
throw new Error("vault: key version out of range");
|
|
25465
|
+
}
|
|
25466
|
+
const bytes = [];
|
|
25467
|
+
let remaining = version2;
|
|
25468
|
+
while (remaining > 0) {
|
|
25469
|
+
bytes.unshift(remaining & 255);
|
|
25470
|
+
remaining = Math.floor(remaining / 256);
|
|
25471
|
+
}
|
|
25472
|
+
return base32Encode(Uint8Array.from(bytes));
|
|
25473
|
+
}
|
|
25474
|
+
function decodeKeyVersion(encoded) {
|
|
25475
|
+
const bytes = base32Decode(encoded);
|
|
25476
|
+
if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
|
|
25477
|
+
let version2 = 0;
|
|
25478
|
+
for (const byte of bytes) version2 = version2 * 256 + byte;
|
|
25479
|
+
if (version2 < 1) throw new Error("vault: bad key version");
|
|
25480
|
+
return version2;
|
|
25481
|
+
}
|
|
25482
|
+
function deriveSubkeys(master) {
|
|
25483
|
+
const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
|
|
25484
|
+
return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
|
|
25485
|
+
}
|
|
25486
|
+
function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
|
|
25487
|
+
if (pointerId.length !== POINTER_ID_BYTES) {
|
|
25488
|
+
throw new Error("vault: pointer id must be 16 bytes");
|
|
25489
|
+
}
|
|
25490
|
+
const head = Buffer.alloc(6);
|
|
25491
|
+
head.writeUInt16BE(formatVersion, 0);
|
|
25492
|
+
head.writeUInt32BE(keyVersion, 2);
|
|
25493
|
+
return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
|
|
25494
|
+
}
|
|
25495
|
+
function seal(encKey, plaintext, aad, nonce) {
|
|
25496
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
|
|
25497
|
+
cipher.setAAD(aad);
|
|
25498
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
25499
|
+
return { ciphertext, nonce, authTag: cipher.getAuthTag() };
|
|
25500
|
+
}
|
|
25501
|
+
function open(encKey, sealed, aad) {
|
|
25502
|
+
try {
|
|
25503
|
+
const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
|
|
25504
|
+
decipher.setAAD(aad);
|
|
25505
|
+
decipher.setAuthTag(sealed.authTag);
|
|
25506
|
+
return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
|
|
25507
|
+
} catch {
|
|
25508
|
+
return null;
|
|
25509
|
+
}
|
|
25510
|
+
}
|
|
25511
|
+
function signPointer(signKey, keyVersion, pointerId, category) {
|
|
25512
|
+
return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
|
|
25513
|
+
}
|
|
25514
|
+
function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
|
|
25515
|
+
if (tag.length !== TAG_BYTES) return false;
|
|
25516
|
+
const expected = signPointer(signKey, keyVersion, pointerId, category);
|
|
25517
|
+
return timingSafeEqual(expected, Buffer.from(tag));
|
|
25518
|
+
}
|
|
25519
|
+
function formatPointer(category, keyVersion, pointerId, tag) {
|
|
25520
|
+
return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
|
|
25521
|
+
}
|
|
25522
|
+
|
|
25523
|
+
// ../../packages/persistence/src/vault/key-provider.ts
|
|
25524
|
+
import { execFileSync } from "child_process";
|
|
25525
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
25526
|
+
import {
|
|
25527
|
+
chmodSync as chmodSync2,
|
|
25528
|
+
mkdirSync as mkdirSync2,
|
|
25529
|
+
readFileSync as readFileSync4,
|
|
25530
|
+
renameSync as renameSync4,
|
|
25531
|
+
rmSync as rmSync4,
|
|
25532
|
+
statSync as statSync3,
|
|
25533
|
+
writeFileSync as writeFileSync3
|
|
25534
|
+
} from "fs";
|
|
25535
|
+
import { join as join6 } from "path";
|
|
25536
|
+
var VAULT_OCCUPANT_REASON = {
|
|
25537
|
+
symlink: "the path is a symlink; remove it so a keyring can be created",
|
|
25538
|
+
gone: "the path was occupied but holds no keyring (removed while it was being created)",
|
|
25539
|
+
unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
|
|
25540
|
+
};
|
|
25541
|
+
var VaultKeyEpochMissingError = class extends Error {
|
|
25542
|
+
version;
|
|
25543
|
+
constructor(version2) {
|
|
25544
|
+
super(`vault: key epoch ${String(version2)} is not present in the keyring`);
|
|
25545
|
+
this.name = "VaultKeyEpochMissingError";
|
|
25546
|
+
this.version = version2;
|
|
25547
|
+
}
|
|
25548
|
+
};
|
|
25549
|
+
var VAULT_KEY_FILENAME = "vault.key";
|
|
25550
|
+
var KEY_MATERIAL_BYTES2 = 32;
|
|
25551
|
+
var KEYCHAIN_SERVICE = "aka-vault";
|
|
25552
|
+
var KEYCHAIN_ACCOUNT = "keyring";
|
|
25553
|
+
function parseKeyring(raw) {
|
|
25554
|
+
const parsed = JSON.parse(raw);
|
|
25555
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
25556
|
+
throw new Error("vault key file is corrupt: not a JSON object");
|
|
25557
|
+
}
|
|
25558
|
+
const { current, keys } = parsed;
|
|
25559
|
+
if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
|
|
25560
|
+
throw new Error("vault key file is corrupt: bad current version");
|
|
25561
|
+
}
|
|
25562
|
+
if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
|
|
25563
|
+
throw new Error("vault key file is corrupt: bad keys map");
|
|
25564
|
+
}
|
|
25565
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
25566
|
+
for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
|
|
25567
|
+
const version2 = Number(rawVersion);
|
|
25568
|
+
if (!Number.isInteger(version2) || version2 < 1) {
|
|
25569
|
+
throw new Error("vault key file is corrupt: bad key version");
|
|
25570
|
+
}
|
|
25571
|
+
if (typeof rawMaterial !== "string") {
|
|
25572
|
+
throw new Error("vault key file is corrupt: bad key material");
|
|
25573
|
+
}
|
|
25574
|
+
const bytes = Buffer.from(rawMaterial, "base64");
|
|
25575
|
+
if (bytes.length !== KEY_MATERIAL_BYTES2) {
|
|
25576
|
+
throw new Error("vault key file is corrupt: bad key material length");
|
|
25577
|
+
}
|
|
25578
|
+
map2.set(version2, bytes);
|
|
25579
|
+
}
|
|
25580
|
+
if (!map2.has(current)) {
|
|
25581
|
+
throw new Error("vault key file is corrupt: current version has no material");
|
|
25582
|
+
}
|
|
25583
|
+
return { current, keys: map2 };
|
|
25584
|
+
}
|
|
25585
|
+
function serializeKeyring(keyring) {
|
|
25586
|
+
const keys = {};
|
|
25587
|
+
for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
|
|
25588
|
+
const material = keyring.keys.get(version2);
|
|
25589
|
+
if (material) keys[String(version2)] = material.toString("base64");
|
|
25590
|
+
}
|
|
25591
|
+
return JSON.stringify({ current: keyring.current, keys });
|
|
25592
|
+
}
|
|
25593
|
+
function mintKeyring() {
|
|
25594
|
+
return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
|
|
25595
|
+
}
|
|
25596
|
+
function withNextEpoch(keyring) {
|
|
25597
|
+
const next = Math.max(...keyring.keys.keys()) + 1;
|
|
25598
|
+
const keys = new Map(keyring.keys);
|
|
25599
|
+
keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
|
|
25600
|
+
return { current: next, keys };
|
|
25601
|
+
}
|
|
25602
|
+
function currentOf(keyring) {
|
|
25603
|
+
const material = keyring.keys.get(keyring.current);
|
|
25604
|
+
if (!material) throw new VaultKeyEpochMissingError(keyring.current);
|
|
25605
|
+
return { material, version: keyring.current };
|
|
25606
|
+
}
|
|
25607
|
+
function epochOf(keyring, version2) {
|
|
25608
|
+
const material = keyring.keys.get(version2);
|
|
25609
|
+
if (!material) throw new VaultKeyEpochMissingError(version2);
|
|
25610
|
+
return { material, version: version2 };
|
|
25611
|
+
}
|
|
25612
|
+
function asAsync(work) {
|
|
25613
|
+
try {
|
|
25614
|
+
return Promise.resolve(work());
|
|
25615
|
+
} catch (err) {
|
|
25616
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
25617
|
+
}
|
|
25618
|
+
}
|
|
25619
|
+
function asError(err) {
|
|
25620
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
25621
|
+
}
|
|
25622
|
+
var ROTATION_LOCK_STALE_MS = 6e4;
|
|
25623
|
+
var LOCK_OWNER_FILE = "owner";
|
|
25624
|
+
var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
|
|
25625
|
+
function claimRotationLock(lock, owner) {
|
|
25626
|
+
try {
|
|
25627
|
+
mkdirSync2(lock);
|
|
25628
|
+
} catch (err) {
|
|
25629
|
+
if (err.code === "EEXIST") return false;
|
|
25630
|
+
throw asError(err);
|
|
25631
|
+
}
|
|
25632
|
+
try {
|
|
25633
|
+
writeFileSync3(join6(lock, LOCK_OWNER_FILE), `${owner}
|
|
25634
|
+
`, { mode: DATA_FILE_MODE });
|
|
25635
|
+
return true;
|
|
25636
|
+
} catch (err) {
|
|
25637
|
+
rmSync4(lock, { recursive: true, force: true });
|
|
25638
|
+
throw asError(err);
|
|
25639
|
+
}
|
|
25640
|
+
}
|
|
25641
|
+
function acquireRotationLock(keysDir2) {
|
|
25642
|
+
const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
25643
|
+
const owner = randomBytes2(16).toString("hex");
|
|
25644
|
+
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
25645
|
+
let held;
|
|
25646
|
+
try {
|
|
25647
|
+
held = statSync3(lock);
|
|
25648
|
+
} catch {
|
|
25649
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
25650
|
+
}
|
|
25651
|
+
if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
|
|
25652
|
+
const aside = `${lock}.stale.${owner}`;
|
|
25653
|
+
try {
|
|
25654
|
+
const now = statSync3(lock);
|
|
25655
|
+
if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
|
|
25656
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
25657
|
+
}
|
|
25658
|
+
renameSync4(lock, aside);
|
|
25659
|
+
} catch (err) {
|
|
25660
|
+
if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
|
|
25661
|
+
throw new Error(ROTATION_IN_PROGRESS, { cause: err });
|
|
25662
|
+
}
|
|
25663
|
+
rmSync4(aside, { recursive: true, force: true });
|
|
25664
|
+
if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
|
|
25665
|
+
return { lock, owner };
|
|
25666
|
+
}
|
|
25667
|
+
function releaseRotationLock(lease) {
|
|
25668
|
+
try {
|
|
25669
|
+
if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
25670
|
+
} catch {
|
|
25671
|
+
return;
|
|
25672
|
+
}
|
|
25673
|
+
rmSync4(lease.lock, { recursive: true, force: true });
|
|
25674
|
+
}
|
|
25675
|
+
function withRotationLock(keysDir2, work) {
|
|
25676
|
+
ensureDataDirSync(keysDir2);
|
|
25677
|
+
const lease = acquireRotationLock(keysDir2);
|
|
25678
|
+
try {
|
|
25679
|
+
return work();
|
|
25680
|
+
} finally {
|
|
25681
|
+
releaseRotationLock(lease);
|
|
25682
|
+
}
|
|
25683
|
+
}
|
|
25684
|
+
var FileKeyProvider = class {
|
|
25685
|
+
#keysDir;
|
|
25686
|
+
constructor(keysDir2) {
|
|
25687
|
+
this.#keysDir = keysDir2;
|
|
25688
|
+
}
|
|
25689
|
+
get filePath() {
|
|
25690
|
+
return join6(this.#keysDir, VAULT_KEY_FILENAME);
|
|
25691
|
+
}
|
|
25692
|
+
loadOrCreate() {
|
|
25693
|
+
return asAsync(() => {
|
|
25694
|
+
const existing = this.#read();
|
|
25695
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
25696
|
+
tightenFileMode(this.filePath);
|
|
25697
|
+
return currentOf(existing);
|
|
25698
|
+
});
|
|
25699
|
+
}
|
|
25700
|
+
rotate() {
|
|
25701
|
+
return asAsync(
|
|
25702
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
25703
|
+
const existing = this.#read();
|
|
25704
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
25705
|
+
return currentOf(this.#write(withNextEpoch(existing)));
|
|
25706
|
+
})
|
|
25707
|
+
);
|
|
25708
|
+
}
|
|
25709
|
+
materialFor(version2) {
|
|
25710
|
+
return asAsync(() => {
|
|
25711
|
+
const existing = this.#read();
|
|
25712
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
25713
|
+
return epochOf(existing, version2);
|
|
25714
|
+
});
|
|
25715
|
+
}
|
|
25716
|
+
/** The keyring, or null when the file is ABSENT. A corrupt file throws. */
|
|
25717
|
+
#read() {
|
|
25718
|
+
let raw;
|
|
25719
|
+
try {
|
|
25720
|
+
raw = readFileSync4(this.filePath, "utf8");
|
|
25721
|
+
} catch (err) {
|
|
25722
|
+
if (err.code === "ENOENT") return null;
|
|
25723
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
25724
|
+
}
|
|
25725
|
+
return parseKeyring(raw);
|
|
25726
|
+
}
|
|
25727
|
+
/**
|
|
25728
|
+
* First mint: the keyring is CREATED, never replaced, so two processes racing
|
|
25729
|
+
* a fresh machine cannot each mint a different epoch 1 — with tmp + rename
|
|
25730
|
+
* the loser's replace would orphan everything the winner had already sealed.
|
|
25731
|
+
* The loser re-reads and adopts the winner's keyring; it minted nothing.
|
|
25732
|
+
*
|
|
25733
|
+
* `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
|
|
25734
|
+
* at the final path, so the keyring never exists at zero length: a reader —
|
|
25735
|
+
* including the loser, re-reading in order to adopt — sees the file absent or
|
|
25736
|
+
* whole, and never mistakes a live keyring for a corrupt one. A corrupt file
|
|
25737
|
+
* still throws from the parse and is never re-minted over.
|
|
25738
|
+
*/
|
|
25739
|
+
#createExclusive() {
|
|
25740
|
+
ensureDataDirSync(this.#keysDir);
|
|
25741
|
+
const keyring = mintKeyring();
|
|
25742
|
+
if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
|
|
25743
|
+
`)) return keyring;
|
|
25744
|
+
const winner = this.#read();
|
|
25745
|
+
if (!winner) {
|
|
25746
|
+
const occupant = classifyOccupant(this.filePath);
|
|
25747
|
+
throw new KeyUnclaimableError(
|
|
25748
|
+
`vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
|
|
25749
|
+
occupant.cause
|
|
25750
|
+
);
|
|
25751
|
+
}
|
|
25752
|
+
tightenFileMode(this.filePath);
|
|
25753
|
+
return winner;
|
|
25754
|
+
}
|
|
25755
|
+
/**
|
|
25756
|
+
* Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
|
|
25757
|
+
* Used only for rotation, under the rotation lock — first creation goes
|
|
25758
|
+
* through the creation-exclusive path instead.
|
|
25759
|
+
*/
|
|
25760
|
+
#write(keyring) {
|
|
25761
|
+
ensureDataDirSync(this.#keysDir);
|
|
25762
|
+
const file2 = this.filePath;
|
|
25763
|
+
const tmp = `${file2}.tmp`;
|
|
25764
|
+
writeFileSync3(tmp, `${serializeKeyring(keyring)}
|
|
25765
|
+
`, { mode: DATA_FILE_MODE });
|
|
25766
|
+
renameSync4(tmp, file2);
|
|
25767
|
+
tightenFileMode(file2);
|
|
25768
|
+
return keyring;
|
|
25769
|
+
}
|
|
25770
|
+
};
|
|
25771
|
+
function tightenFileMode(file2) {
|
|
25772
|
+
try {
|
|
25773
|
+
chmodSync2(file2, DATA_FILE_MODE);
|
|
25774
|
+
} catch {
|
|
25775
|
+
}
|
|
25776
|
+
}
|
|
25777
|
+
var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
|
|
25778
|
+
encoding: "utf8",
|
|
25779
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
25780
|
+
});
|
|
25781
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
25782
|
+
var KeychainKeyProvider = class {
|
|
25783
|
+
#keysDir;
|
|
25784
|
+
#exec;
|
|
25785
|
+
constructor(keysDir2, exec = runSecurity) {
|
|
25786
|
+
if (exec === runSecurity && process.platform !== "darwin") {
|
|
25787
|
+
throw new Error(
|
|
25788
|
+
`keychain custody is not available on this platform (${process.platform}); use file custody`
|
|
25789
|
+
);
|
|
25790
|
+
}
|
|
25791
|
+
this.#keysDir = keysDir2;
|
|
25792
|
+
this.#exec = exec;
|
|
25793
|
+
}
|
|
25794
|
+
/** Where a fallback file provider for the same vault would keep its keyring. */
|
|
25795
|
+
get keysDir() {
|
|
25796
|
+
return this.#keysDir;
|
|
25797
|
+
}
|
|
25798
|
+
loadOrCreate() {
|
|
25799
|
+
return asAsync(() => {
|
|
25800
|
+
const existing = this.#read();
|
|
25801
|
+
if (existing) return currentOf(existing);
|
|
25802
|
+
return currentOf(this.#create(mintKeyring()));
|
|
25803
|
+
});
|
|
25804
|
+
}
|
|
25805
|
+
rotate() {
|
|
25806
|
+
return asAsync(
|
|
25807
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
25808
|
+
const existing = this.#read();
|
|
25809
|
+
if (!existing) return currentOf(this.#create(mintKeyring()));
|
|
25810
|
+
return currentOf(this.#replace(withNextEpoch(existing)));
|
|
25811
|
+
})
|
|
25812
|
+
);
|
|
25813
|
+
}
|
|
25814
|
+
materialFor(version2) {
|
|
25815
|
+
return asAsync(() => {
|
|
25816
|
+
const existing = this.#read();
|
|
25817
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
25818
|
+
return epochOf(existing, version2);
|
|
25819
|
+
});
|
|
25820
|
+
}
|
|
25821
|
+
/** The keyring, or null when no item exists yet. A corrupt item throws. */
|
|
25822
|
+
#read() {
|
|
25823
|
+
let raw;
|
|
25824
|
+
try {
|
|
25825
|
+
raw = this.#exec([
|
|
25826
|
+
"find-generic-password",
|
|
25827
|
+
"-s",
|
|
25828
|
+
KEYCHAIN_SERVICE,
|
|
25829
|
+
"-a",
|
|
25830
|
+
KEYCHAIN_ACCOUNT,
|
|
25831
|
+
"-w"
|
|
25832
|
+
]);
|
|
25833
|
+
} catch (err) {
|
|
25834
|
+
if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
|
|
25835
|
+
throw new Error(
|
|
25836
|
+
`vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
|
|
25837
|
+
{ cause: err }
|
|
25838
|
+
);
|
|
25839
|
+
}
|
|
25840
|
+
const body = raw.trim();
|
|
25841
|
+
if (body.length === 0) return null;
|
|
25842
|
+
return parseKeyring(body);
|
|
25843
|
+
}
|
|
25844
|
+
/**
|
|
25845
|
+
* First mint: a plain `add-generic-password` (no `-U`) fails when an item
|
|
25846
|
+
* already exists, so a concurrent first mint cannot overwrite the winner's
|
|
25847
|
+
* keyring — the loser re-reads and adopts it instead.
|
|
25848
|
+
*/
|
|
25849
|
+
#create(keyring) {
|
|
25850
|
+
const args = [
|
|
25851
|
+
"add-generic-password",
|
|
25852
|
+
"-s",
|
|
25853
|
+
KEYCHAIN_SERVICE,
|
|
25854
|
+
"-a",
|
|
25855
|
+
KEYCHAIN_ACCOUNT,
|
|
25856
|
+
"-w",
|
|
25857
|
+
serializeKeyring(keyring)
|
|
25858
|
+
];
|
|
25859
|
+
try {
|
|
25860
|
+
this.#exec(args);
|
|
25861
|
+
} catch (err) {
|
|
25862
|
+
const winner = this.#read();
|
|
25863
|
+
if (winner) return winner;
|
|
25864
|
+
throw asError(err);
|
|
25865
|
+
}
|
|
25866
|
+
return keyring;
|
|
25867
|
+
}
|
|
25868
|
+
// `-U` updates the item in place, deliberately replacing the stored map with
|
|
25869
|
+
// one that contains it — used only for rotation, under the rotation lock.
|
|
25870
|
+
#replace(keyring) {
|
|
25871
|
+
this.#exec([
|
|
25872
|
+
"add-generic-password",
|
|
25873
|
+
"-U",
|
|
25874
|
+
"-s",
|
|
25875
|
+
KEYCHAIN_SERVICE,
|
|
25876
|
+
"-a",
|
|
25877
|
+
KEYCHAIN_ACCOUNT,
|
|
25878
|
+
"-w",
|
|
25879
|
+
serializeKeyring(keyring)
|
|
25880
|
+
]);
|
|
25881
|
+
return keyring;
|
|
25882
|
+
}
|
|
25883
|
+
};
|
|
25884
|
+
function createKeyProvider(custody, keysDir2) {
|
|
25885
|
+
if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
|
|
25886
|
+
return new FileKeyProvider(keysDir2);
|
|
25887
|
+
}
|
|
25888
|
+
|
|
25889
|
+
// ../../packages/persistence/src/vault/vault.ts
|
|
25890
|
+
import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
|
|
25891
|
+
var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
|
|
25892
|
+
var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
|
|
25893
|
+
var VAULT_PURGE_POINTER_ID = "*";
|
|
25894
|
+
function parsePointer(token) {
|
|
25895
|
+
if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
|
|
25896
|
+
const body = token.slice("[[aka:".length, -"]]".length);
|
|
25897
|
+
const colon = body.indexOf(":");
|
|
25898
|
+
if (colon < 0) return null;
|
|
25899
|
+
const category = body.slice(0, colon);
|
|
25900
|
+
const [kv, id, tag] = body.slice(colon + 1).split(".");
|
|
25901
|
+
if (kv === void 0 || id === void 0 || tag === void 0) return null;
|
|
25902
|
+
try {
|
|
25903
|
+
const keyVersion = decodeKeyVersion(kv);
|
|
25904
|
+
const pointerId = base32Decode(id);
|
|
25905
|
+
const tagBytes = base32Decode(tag);
|
|
25906
|
+
if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
|
|
25907
|
+
return null;
|
|
25908
|
+
}
|
|
25909
|
+
return { category, keyVersion, pointerId, tag: tagBytes };
|
|
25910
|
+
} catch {
|
|
25911
|
+
return null;
|
|
25912
|
+
}
|
|
25913
|
+
}
|
|
25914
|
+
var SecretVault = class {
|
|
25915
|
+
#repo;
|
|
25916
|
+
#keys;
|
|
25917
|
+
#isConsented;
|
|
25918
|
+
#verifyGrant;
|
|
25919
|
+
#now;
|
|
25920
|
+
constructor(deps) {
|
|
25921
|
+
this.#repo = deps.repo;
|
|
25922
|
+
this.#keys = deps.keys;
|
|
25923
|
+
this.#isConsented = deps.isConsented;
|
|
25924
|
+
this.#verifyGrant = deps.verifyGrant;
|
|
25925
|
+
this.#now = deps.now ?? (() => Date.now());
|
|
25926
|
+
}
|
|
25927
|
+
/**
|
|
25928
|
+
* Store a value and return the pointer that stands for it. The same value
|
|
25929
|
+
* always yields the same pointer on this machine — one row, one pointer id,
|
|
25930
|
+
* one category — which is what makes dedup and reuse counting work.
|
|
25931
|
+
*
|
|
25932
|
+
* `fingerprintKey` is the exception-key epoch this value's fingerprint is
|
|
25933
|
+
* derived under — a different key from the vault's, with different rotation
|
|
25934
|
+
* semantics. It is a parameter of the WRITE rather than a constructor dep,
|
|
25935
|
+
* and a thunk rather than a value, so that the only way to reach a key is to
|
|
25936
|
+
* store something: a read-only caller never names it, and a caller whose
|
|
25937
|
+
* source mints on absence mints only once consent has actually opened the
|
|
25938
|
+
* write. `refreshFingerprints` takes its key the same way, for the same
|
|
25939
|
+
* reason.
|
|
25940
|
+
*
|
|
25941
|
+
* Resolved once per call, so the fingerprint and the version it is recorded
|
|
25942
|
+
* under can never come from two different epochs.
|
|
25943
|
+
*/
|
|
25944
|
+
async tokenize(raw, meta3, fingerprintKey) {
|
|
25945
|
+
if (!this.#isConsented()) return CONSENT_ABSENT;
|
|
25946
|
+
const fpKey = fingerprintKey();
|
|
25947
|
+
const valueFingerprint = fingerprintValue(fpKey, raw);
|
|
25948
|
+
const existing = this.#repo.byValueFingerprint(valueFingerprint);
|
|
25949
|
+
const now = this.#now();
|
|
25950
|
+
if (existing) {
|
|
25951
|
+
this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
|
|
25952
|
+
return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
|
|
25953
|
+
}
|
|
25954
|
+
const { material, version: version2 } = await this.#keys.loadOrCreate();
|
|
25955
|
+
const subkeys = deriveSubkeys(material);
|
|
25956
|
+
const pointerId = randomBytes3(POINTER_ID_BYTES);
|
|
25957
|
+
const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
|
|
25958
|
+
const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
|
|
25959
|
+
const { row } = this.#repo.upsert(
|
|
25960
|
+
{
|
|
25961
|
+
pointerId: base32Encode(pointerId),
|
|
25962
|
+
valueFingerprint,
|
|
25963
|
+
fingerprintKeyVersion: fpKey.version,
|
|
25964
|
+
keyVersion: version2,
|
|
25965
|
+
// Recorded so the row stays OPENABLE if the wire-format constant ever
|
|
25966
|
+
// moves: it is part of this row's AEAD AAD. It is not a tag input —
|
|
25967
|
+
// tags are pinned to the constant on both sides.
|
|
25968
|
+
formatVersion: POINTER_FORMAT_VERSION,
|
|
25969
|
+
category: meta3.category,
|
|
25970
|
+
ruleId: meta3.ruleId,
|
|
25971
|
+
maskedMatch: meta3.maskedMatch,
|
|
25972
|
+
provider: meta3.provider,
|
|
25973
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
25974
|
+
nonce: sealed.nonce.toString("base64"),
|
|
25975
|
+
authTag: sealed.authTag.toString("base64")
|
|
25976
|
+
},
|
|
25977
|
+
now
|
|
25978
|
+
);
|
|
25979
|
+
return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
|
|
25980
|
+
}
|
|
25981
|
+
/**
|
|
25982
|
+
* Resolve a pointer back to its value, for a human or (with a grant) for the
|
|
25983
|
+
* model. Every call that gets as far as an identified row writes an audit row.
|
|
25984
|
+
*/
|
|
25985
|
+
async detokenize(token, opts) {
|
|
25986
|
+
const parsed = parsePointer(token);
|
|
25987
|
+
if (!parsed) return UNAVAILABLE;
|
|
25988
|
+
let signKey;
|
|
25989
|
+
try {
|
|
25990
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
25991
|
+
signKey = deriveSubkeys(epoch.material).sign;
|
|
25992
|
+
} catch {
|
|
25993
|
+
return UNAVAILABLE;
|
|
25994
|
+
}
|
|
25995
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
25996
|
+
return UNAVAILABLE;
|
|
25997
|
+
}
|
|
25998
|
+
const pointerId = base32Encode(parsed.pointerId);
|
|
25999
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
26000
|
+
if (!row) {
|
|
26001
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
26002
|
+
return UNAVAILABLE;
|
|
26003
|
+
}
|
|
26004
|
+
if (row.category !== parsed.category) return UNAVAILABLE;
|
|
26005
|
+
if (opts.target === "model") {
|
|
26006
|
+
const grantId = opts.grantId;
|
|
26007
|
+
const verify = this.#verifyGrant;
|
|
26008
|
+
if (verify === void 0 || grantId === void 0 || grantId === "") {
|
|
26009
|
+
this.#audit(pointerId, opts, "refused");
|
|
26010
|
+
return UNAVAILABLE;
|
|
26011
|
+
}
|
|
26012
|
+
let covered;
|
|
26013
|
+
try {
|
|
26014
|
+
covered = await verify(grantId, {
|
|
26015
|
+
ruleId: row.ruleId,
|
|
26016
|
+
valueFingerprint: row.valueFingerprint,
|
|
26017
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
26018
|
+
});
|
|
26019
|
+
} catch {
|
|
26020
|
+
covered = false;
|
|
26021
|
+
}
|
|
26022
|
+
if (!covered) {
|
|
26023
|
+
this.#audit(pointerId, opts, "refused");
|
|
26024
|
+
return UNAVAILABLE;
|
|
26025
|
+
}
|
|
26026
|
+
}
|
|
26027
|
+
let raw;
|
|
26028
|
+
try {
|
|
26029
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
26030
|
+
raw = open(
|
|
26031
|
+
deriveSubkeys(epoch.material).enc,
|
|
26032
|
+
{
|
|
26033
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
26034
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
26035
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
26036
|
+
},
|
|
26037
|
+
// Sealed under the ROW's epoch and format version. Rotation may have
|
|
26038
|
+
// moved the epoch past the one this token names, and a format bump may
|
|
26039
|
+
// have moved the constant past the generation this row was sealed
|
|
26040
|
+
// under — the AAD follows the row in both cases, never the token.
|
|
26041
|
+
bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
|
|
26042
|
+
);
|
|
26043
|
+
} catch {
|
|
26044
|
+
raw = null;
|
|
26045
|
+
}
|
|
26046
|
+
if (raw === null) {
|
|
26047
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
26048
|
+
return UNAVAILABLE;
|
|
26049
|
+
}
|
|
26050
|
+
this.#audit(pointerId, opts, "revealed");
|
|
26051
|
+
return raw;
|
|
26052
|
+
}
|
|
26053
|
+
/**
|
|
26054
|
+
* Owner-surface reveal by row id: the dashboard shows a row the owner can
|
|
26055
|
+
* already see and asks for its value. There is no wire token here to verify —
|
|
26056
|
+
* the tag exists to stop FORGED tokens arriving in untrusted text, and a row
|
|
26057
|
+
* id selected server-side from the owner's own store is not that — so this
|
|
26058
|
+
* loads the row directly, opens its ciphertext under the row's epoch, and
|
|
26059
|
+
* audits exactly like a human-target de-reference. Never callable with
|
|
26060
|
+
* target 'model': the wire-token path with its grant gate is the only road
|
|
26061
|
+
* raw travels toward the model.
|
|
26062
|
+
*/
|
|
26063
|
+
async revealEntry(pointerId, opts) {
|
|
26064
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
26065
|
+
if (!row) {
|
|
26066
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
26067
|
+
return UNAVAILABLE;
|
|
26068
|
+
}
|
|
26069
|
+
const raw = await this.#openRow(row);
|
|
26070
|
+
if (raw === null) {
|
|
26071
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
26072
|
+
return UNAVAILABLE;
|
|
26073
|
+
}
|
|
26074
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
|
|
26075
|
+
return raw;
|
|
26076
|
+
}
|
|
26077
|
+
/** Badge and listing data. No raw value, no fingerprint, and no audit row. */
|
|
26078
|
+
async describePointer(token) {
|
|
26079
|
+
const row = await this.#rowFor(token);
|
|
26080
|
+
if (!row) return null;
|
|
26081
|
+
return {
|
|
26082
|
+
category: row.category,
|
|
26083
|
+
...row.provider === void 0 ? {} : { provider: row.provider },
|
|
26084
|
+
maskedMatch: row.maskedMatch,
|
|
26085
|
+
occurrences: row.occurrenceCount,
|
|
26086
|
+
firstSeen: new Date(row.firstSeen).toISOString(),
|
|
26087
|
+
lastSeen: new Date(row.lastSeen).toISOString()
|
|
26088
|
+
};
|
|
26089
|
+
}
|
|
26090
|
+
/**
|
|
26091
|
+
* The raw-free row identity a reveal grant matches on. Deliberately not fed to
|
|
26092
|
+
* view surfaces: the keyed fingerprint is a correlation key and must not reach
|
|
26093
|
+
* a presentation layer.
|
|
26094
|
+
*/
|
|
26095
|
+
async resolvePointerIdentity(token) {
|
|
26096
|
+
const row = await this.#rowFor(token);
|
|
26097
|
+
if (!row) return null;
|
|
26098
|
+
return {
|
|
26099
|
+
ruleId: row.ruleId,
|
|
26100
|
+
valueFingerprint: row.valueFingerprint,
|
|
26101
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
26102
|
+
};
|
|
26103
|
+
}
|
|
26104
|
+
/**
|
|
26105
|
+
* Mint the next vault key epoch and re-encrypt every entry under it. Pointers
|
|
26106
|
+
* already emitted keep verifying: their tag is checked against the historical
|
|
26107
|
+
* epoch they name, which the key provider retains.
|
|
26108
|
+
*
|
|
26109
|
+
* Safe to interrupt — each row carries the epoch its ciphertext is sealed
|
|
26110
|
+
* under, so a half-finished pass leaves every row openable.
|
|
26111
|
+
*
|
|
26112
|
+
* The rotation lock covers only the keyring mint inside `rotate()`; the
|
|
26113
|
+
* re-seal pass below runs unlocked. Two concurrent rotations therefore
|
|
26114
|
+
* serialize on the keyring but interleave over the rows, so a slower pass can
|
|
26115
|
+
* re-seal a row back to an epoch a faster one already moved past, and
|
|
26116
|
+
* `reEncrypted` can double-count. No value is lost either way — every epoch is
|
|
26117
|
+
* retained and every row stays openable — but "after rotation every row sits
|
|
26118
|
+
* at the newest epoch" does not hold under concurrency. Holding the lock
|
|
26119
|
+
* across the whole pass requires an async-aware lock, since a callback that
|
|
26120
|
+
* awaits would release the lock at its first suspension.
|
|
26121
|
+
*/
|
|
26122
|
+
async rotateVaultKey() {
|
|
26123
|
+
const next = await this.#keys.rotate();
|
|
26124
|
+
const nextEnc = deriveSubkeys(next.material).enc;
|
|
26125
|
+
let reEncrypted = 0;
|
|
26126
|
+
for (const row of this.#repo.listAll()) {
|
|
26127
|
+
if (row.keyVersion === next.version) continue;
|
|
26128
|
+
const pointerId = base32Decode(row.pointerId);
|
|
26129
|
+
let raw;
|
|
26130
|
+
try {
|
|
26131
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
26132
|
+
raw = open(
|
|
26133
|
+
deriveSubkeys(epoch.material).enc,
|
|
26134
|
+
{
|
|
26135
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
26136
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
26137
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
26138
|
+
},
|
|
26139
|
+
bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
|
|
26140
|
+
);
|
|
26141
|
+
} catch {
|
|
26142
|
+
raw = null;
|
|
26143
|
+
}
|
|
26144
|
+
if (raw === null) continue;
|
|
26145
|
+
const sealed = seal(
|
|
26146
|
+
nextEnc,
|
|
26147
|
+
raw,
|
|
26148
|
+
bindingInput(next.version, pointerId, row.category, row.formatVersion),
|
|
26149
|
+
randomBytes3(NONCE_BYTES)
|
|
26150
|
+
);
|
|
26151
|
+
this.#repo.replaceCiphertext(row.pointerId, {
|
|
26152
|
+
keyVersion: next.version,
|
|
26153
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
26154
|
+
nonce: sealed.nonce.toString("base64"),
|
|
26155
|
+
authTag: sealed.authTag.toString("base64")
|
|
26156
|
+
});
|
|
26157
|
+
reEncrypted += 1;
|
|
26158
|
+
}
|
|
26159
|
+
return { version: next.version, reEncrypted };
|
|
26160
|
+
}
|
|
26161
|
+
/**
|
|
26162
|
+
* Re-key every entry's value fingerprint after the exception key rotates,
|
|
26163
|
+
* PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
|
|
26164
|
+
* because the raw values are gone — the vault still holds the values, so
|
|
26165
|
+
* determinism, dedup, and every outstanding pointer survive the rotation.
|
|
26166
|
+
*
|
|
26167
|
+
* Every fingerprint-key rotation must run this: a row left at the old epoch
|
|
26168
|
+
* still resolves, but the same value detected again fingerprints under the
|
|
26169
|
+
* NEW key, misses the dedup lookup, and mints a second row and a second
|
|
26170
|
+
* pointer — one value, two tokens in circulation.
|
|
26171
|
+
*
|
|
26172
|
+
* Per-row best-effort: a row that cannot open, or whose refreshed
|
|
26173
|
+
* fingerprint collides with a row already refreshed, is skipped rather than
|
|
26174
|
+
* aborting the pass — one damaged entry must not strand the re-key of every
|
|
26175
|
+
* other. A skipped row keeps resolving under its old fingerprint epoch.
|
|
26176
|
+
*/
|
|
26177
|
+
async refreshFingerprints(next) {
|
|
26178
|
+
let refreshed = 0;
|
|
26179
|
+
for (const row of this.#repo.listAll()) {
|
|
26180
|
+
try {
|
|
26181
|
+
const raw = await this.#openRow(row);
|
|
26182
|
+
if (raw === null) continue;
|
|
26183
|
+
this.#repo.refreshFingerprint(row.pointerId, {
|
|
26184
|
+
valueFingerprint: fingerprintValue(next, raw),
|
|
26185
|
+
fingerprintKeyVersion: next.version
|
|
26186
|
+
});
|
|
26187
|
+
refreshed += 1;
|
|
26188
|
+
} catch {
|
|
26189
|
+
continue;
|
|
26190
|
+
}
|
|
26191
|
+
}
|
|
26192
|
+
return refreshed;
|
|
26193
|
+
}
|
|
26194
|
+
/**
|
|
26195
|
+
* Destroy every entry, making all outstanding pointers permanently
|
|
26196
|
+
* unresolvable.
|
|
26197
|
+
*
|
|
26198
|
+
* The count comes from `purgeAll` rather than a separate `countEntries` —
|
|
26199
|
+
* `purgeAll` counts inside the same transaction that deletes, so the audit row
|
|
26200
|
+
* reports what was actually destroyed. Counting beforehand would let a
|
|
26201
|
+
* concurrent write land between the two statements and put a number in the
|
|
26202
|
+
* durable record that never matched reality.
|
|
26203
|
+
*/
|
|
26204
|
+
purgeVault() {
|
|
26205
|
+
const destroyed = this.#repo.purgeAll();
|
|
26206
|
+
this.#repo.recordDeref({
|
|
26207
|
+
id: randomUUID12(),
|
|
26208
|
+
pointerId: VAULT_PURGE_POINTER_ID,
|
|
26209
|
+
at: this.#now(),
|
|
26210
|
+
target: "human",
|
|
26211
|
+
reason: "purge",
|
|
26212
|
+
outcome: "unavailable",
|
|
26213
|
+
pointerCount: Math.max(destroyed, 1)
|
|
26214
|
+
});
|
|
26215
|
+
return destroyed;
|
|
26216
|
+
}
|
|
26217
|
+
// Sign under the epoch the token names — which for a re-detected value is the
|
|
26218
|
+
// epoch its row currently sits at rather than whatever is current.
|
|
26219
|
+
//
|
|
26220
|
+
// The row's format version is NOT a tag input. It binds the row's ciphertext
|
|
26221
|
+
// (it is part of the AEAD AAD, so an old row stays openable) but never the
|
|
26222
|
+
// wire tag, which verification checks against POINTER_FORMAT_VERSION without
|
|
26223
|
+
// knowing any row. Signing a token here under a row's own generation is what
|
|
26224
|
+
// would make the vault emit tokens it then refuses.
|
|
26225
|
+
async #emitToken(keyVersion, pointerIdB32, category) {
|
|
26226
|
+
const pointerId = base32Decode(pointerIdB32);
|
|
26227
|
+
const epoch = await this.#keys.materialFor(keyVersion);
|
|
26228
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
26229
|
+
return formatPointer(
|
|
26230
|
+
category,
|
|
26231
|
+
keyVersion,
|
|
26232
|
+
pointerId,
|
|
26233
|
+
signPointer(signKey, keyVersion, pointerId, category)
|
|
26234
|
+
);
|
|
26235
|
+
}
|
|
26236
|
+
async #openRow(row) {
|
|
26237
|
+
try {
|
|
26238
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
26239
|
+
return open(
|
|
26240
|
+
deriveSubkeys(epoch.material).enc,
|
|
26241
|
+
{
|
|
26242
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
26243
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
26244
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
26245
|
+
},
|
|
26246
|
+
bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
|
|
26247
|
+
);
|
|
26248
|
+
} catch {
|
|
26249
|
+
return null;
|
|
26250
|
+
}
|
|
26251
|
+
}
|
|
26252
|
+
// Shared lookup for the read-only surfaces. It verifies the tag exactly as
|
|
26253
|
+
// detokenize does: a descriptor is not raw, but a token nobody can vouch for
|
|
26254
|
+
// should not resolve to anything at all — otherwise a fabricated pointer, or a
|
|
26255
|
+
// lookalike planted in a file, would still yield a category and a masked
|
|
26256
|
+
// preview. Verifying needs the historical epoch's key, which is why these
|
|
26257
|
+
// surfaces are async.
|
|
26258
|
+
async #rowFor(token) {
|
|
26259
|
+
const parsed = parsePointer(token);
|
|
26260
|
+
if (!parsed) return null;
|
|
26261
|
+
try {
|
|
26262
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
26263
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
26264
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
26265
|
+
return null;
|
|
26266
|
+
}
|
|
26267
|
+
} catch {
|
|
26268
|
+
return null;
|
|
26269
|
+
}
|
|
26270
|
+
const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
|
|
26271
|
+
if (row?.category !== parsed.category) return null;
|
|
26272
|
+
return row;
|
|
26273
|
+
}
|
|
26274
|
+
#audit(pointerId, opts, outcome) {
|
|
26275
|
+
this.#repo.recordDeref({
|
|
26276
|
+
id: randomUUID12(),
|
|
26277
|
+
pointerId,
|
|
26278
|
+
at: this.#now(),
|
|
26279
|
+
target: opts.target,
|
|
26280
|
+
reason: opts.reason,
|
|
26281
|
+
outcome,
|
|
26282
|
+
...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
|
|
26283
|
+
// Only the batched reasons carry a count above one; a model crossing is
|
|
26284
|
+
// always its own row.
|
|
26285
|
+
pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
|
|
26286
|
+
});
|
|
26287
|
+
}
|
|
26288
|
+
};
|
|
26289
|
+
|
|
23778
26290
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
23779
|
-
import { existsSync as
|
|
23780
|
-
import { join as
|
|
26291
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
26292
|
+
import { join as join7 } from "path";
|
|
23781
26293
|
var MARKER = "warn-era-capped";
|
|
23782
26294
|
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
23783
26295
|
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
23784
|
-
const marker =
|
|
23785
|
-
if (
|
|
26296
|
+
const marker = join7(dataDir2, MARKER);
|
|
26297
|
+
if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
|
|
23786
26298
|
const capped = db.policies.capCategoryActions();
|
|
23787
|
-
|
|
26299
|
+
writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
|
|
23788
26300
|
`, { mode: DATA_FILE_MODE });
|
|
23789
26301
|
return { capped };
|
|
23790
26302
|
}
|
|
@@ -23848,11 +26360,11 @@ function providerFromModelId(modelId) {
|
|
|
23848
26360
|
}
|
|
23849
26361
|
|
|
23850
26362
|
// ../../packages/plugin-sdk/src/config.ts
|
|
23851
|
-
function loadConfig(base = defaultDataDir()) {
|
|
26363
|
+
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
23852
26364
|
try {
|
|
23853
26365
|
ensureLayoutDirSync(base);
|
|
23854
|
-
const settingsFile =
|
|
23855
|
-
if (
|
|
26366
|
+
const settingsFile = join8(settingsDir(base), "settings.json");
|
|
26367
|
+
if (existsSync5(settingsFile)) tightenFile(settingsFile);
|
|
23856
26368
|
} catch {
|
|
23857
26369
|
}
|
|
23858
26370
|
migrateLegacyLayout(base);
|
|
@@ -23863,21 +26375,21 @@ function loadConfig(base = defaultDataDir()) {
|
|
|
23863
26375
|
dbPath: dbPath(base),
|
|
23864
26376
|
settingsDir: settingsDir(base),
|
|
23865
26377
|
onboarded: settings.onboardedAt != null,
|
|
23866
|
-
provider: resolveProviderSafe()
|
|
26378
|
+
provider: resolveProviderSafe(resolveProviderFn)
|
|
23867
26379
|
};
|
|
23868
26380
|
}
|
|
23869
|
-
function resolveProviderSafe() {
|
|
26381
|
+
function resolveProviderSafe(resolveProviderFn) {
|
|
23870
26382
|
try {
|
|
23871
|
-
return
|
|
26383
|
+
return resolveProviderFn();
|
|
23872
26384
|
} catch {
|
|
23873
26385
|
return { provider: "anthropic" };
|
|
23874
26386
|
}
|
|
23875
26387
|
}
|
|
23876
26388
|
|
|
23877
26389
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23878
|
-
import { readdirSync, readFileSync as
|
|
26390
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
|
|
23879
26391
|
import { homedir as homedir2 } from "os";
|
|
23880
|
-
import { basename as
|
|
26392
|
+
import { basename as basename3, join as join10 } from "path";
|
|
23881
26393
|
|
|
23882
26394
|
// ../../packages/detections/src/egress/registry.ts
|
|
23883
26395
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -24665,12 +27177,12 @@ function redact(text, findings) {
|
|
|
24665
27177
|
const regions = [];
|
|
24666
27178
|
for (const f of sorted) {
|
|
24667
27179
|
const rank = SEVERITY_RANK2[f.severity];
|
|
24668
|
-
const
|
|
24669
|
-
if (
|
|
24670
|
-
|
|
24671
|
-
if (rank >
|
|
24672
|
-
|
|
24673
|
-
|
|
27180
|
+
const open2 = regions[regions.length - 1];
|
|
27181
|
+
if (open2 && f.span.start < open2.end) {
|
|
27182
|
+
open2.end = Math.max(open2.end, f.span.end);
|
|
27183
|
+
if (rank > open2.rank) {
|
|
27184
|
+
open2.rank = rank;
|
|
27185
|
+
open2.category = f.category;
|
|
24674
27186
|
}
|
|
24675
27187
|
} else {
|
|
24676
27188
|
regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
|
|
@@ -24699,6 +27211,24 @@ function maskMatch(raw) {
|
|
|
24699
27211
|
return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
|
|
24700
27212
|
}
|
|
24701
27213
|
|
|
27214
|
+
// ../../packages/detections/src/pointer-shield.ts
|
|
27215
|
+
function shieldPointers(text) {
|
|
27216
|
+
const spans = [];
|
|
27217
|
+
let out = null;
|
|
27218
|
+
for (const match of text.matchAll(pointerTokenScanner())) {
|
|
27219
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
27220
|
+
out ??= text;
|
|
27221
|
+
out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
|
|
27222
|
+
}
|
|
27223
|
+
return { text: out ?? text, spans };
|
|
27224
|
+
}
|
|
27225
|
+
function dropShieldedFindings(findings, spans) {
|
|
27226
|
+
if (spans.length === 0) return findings;
|
|
27227
|
+
return findings.filter(
|
|
27228
|
+
(finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
|
|
27229
|
+
);
|
|
27230
|
+
}
|
|
27231
|
+
|
|
24702
27232
|
// ../../packages/detections/src/posture/config-posture.ts
|
|
24703
27233
|
var RULE_VERSION = "1";
|
|
24704
27234
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -26435,7 +28965,7 @@ var gcp_service_account_default = {
|
|
|
26435
28965
|
severity: "critical",
|
|
26436
28966
|
matcher: {
|
|
26437
28967
|
type: "regex",
|
|
26438
|
-
pattern: "
|
|
28968
|
+
pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
|
|
26439
28969
|
flags: "g"
|
|
26440
28970
|
},
|
|
26441
28971
|
examples: [
|
|
@@ -26832,7 +29362,8 @@ function scanText(text, ruleVersions) {
|
|
|
26832
29362
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
26833
29363
|
try {
|
|
26834
29364
|
const rules = getLoadedRules();
|
|
26835
|
-
const
|
|
29365
|
+
const shielded = shieldPointers(text);
|
|
29366
|
+
const matches = dropShieldedFindings(scan(shielded.text, rules), shielded.spans);
|
|
26836
29367
|
if (matches.length === 0) return { masked: text, findings: [] };
|
|
26837
29368
|
const byId = new Map(rules.map((r) => [r.id, r]));
|
|
26838
29369
|
const findings = matches.map((m) => {
|
|
@@ -26855,8 +29386,8 @@ function scanText(text, ruleVersions) {
|
|
|
26855
29386
|
}
|
|
26856
29387
|
|
|
26857
29388
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
26858
|
-
import { existsSync as
|
|
26859
|
-
import { basename, dirname, isAbsolute, join as
|
|
29389
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
29390
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
|
|
26860
29391
|
function resolveRepoIdentity(cwd) {
|
|
26861
29392
|
try {
|
|
26862
29393
|
const root = findGitRoot(cwd);
|
|
@@ -26869,7 +29400,7 @@ function resolveRepoIdentity(cwd) {
|
|
|
26869
29400
|
// win32) so the persistence layer's `/`-separated checkout-path patterns
|
|
26870
29401
|
// (the ghost sweep + the read-side worktree filter) match it as written.
|
|
26871
29402
|
url: url2 ?? headRoot.split(sep2).join("/"),
|
|
26872
|
-
name: (url2 ? slugFromUrl(url2) : void 0) ??
|
|
29403
|
+
name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
|
|
26873
29404
|
};
|
|
26874
29405
|
} catch {
|
|
26875
29406
|
return void 0;
|
|
@@ -26889,36 +29420,36 @@ function resolveRepoNwo(cwd) {
|
|
|
26889
29420
|
function findGitRoot(start) {
|
|
26890
29421
|
let dir = start;
|
|
26891
29422
|
for (; ; ) {
|
|
26892
|
-
if (
|
|
26893
|
-
const parent =
|
|
29423
|
+
if (existsSync6(join9(dir, ".git"))) return dir;
|
|
29424
|
+
const parent = dirname2(dir);
|
|
26894
29425
|
if (parent === dir) return void 0;
|
|
26895
29426
|
dir = parent;
|
|
26896
29427
|
}
|
|
26897
29428
|
}
|
|
26898
29429
|
function resolveGitContext(root) {
|
|
26899
|
-
const dotGit =
|
|
29430
|
+
const dotGit = join9(root, ".git");
|
|
26900
29431
|
try {
|
|
26901
|
-
if (
|
|
26902
|
-
return { configPath:
|
|
29432
|
+
if (statSync4(dotGit).isDirectory()) {
|
|
29433
|
+
return { configPath: join9(dotGit, "config"), headRoot: root };
|
|
26903
29434
|
}
|
|
26904
29435
|
} catch {
|
|
26905
29436
|
return void 0;
|
|
26906
29437
|
}
|
|
26907
29438
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
26908
29439
|
if (!target) return void 0;
|
|
26909
|
-
const gitdir = isAbsolute(target) ? target :
|
|
26910
|
-
if (
|
|
26911
|
-
return { configPath:
|
|
29440
|
+
const gitdir = isAbsolute(target) ? target : join9(root, target);
|
|
29441
|
+
if (existsSync6(join9(gitdir, "config"))) {
|
|
29442
|
+
return { configPath: join9(gitdir, "config"), headRoot: root };
|
|
26912
29443
|
}
|
|
26913
|
-
const commonRaw = safeRead(
|
|
29444
|
+
const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
|
|
26914
29445
|
if (!commonRaw) return void 0;
|
|
26915
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
26916
|
-
const headRoot =
|
|
26917
|
-
return { configPath:
|
|
29446
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
|
|
29447
|
+
const headRoot = basename2(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
|
|
29448
|
+
return { configPath: join9(commonGitDir, "config"), headRoot };
|
|
26918
29449
|
}
|
|
26919
29450
|
function safeRead(path) {
|
|
26920
29451
|
try {
|
|
26921
|
-
return
|
|
29452
|
+
return readFileSync5(path, "utf8");
|
|
26922
29453
|
} catch {
|
|
26923
29454
|
return void 0;
|
|
26924
29455
|
}
|
|
@@ -26969,18 +29500,23 @@ function nwoFromUrl(url2) {
|
|
|
26969
29500
|
}
|
|
26970
29501
|
|
|
26971
29502
|
// ../../packages/plugin-sdk/src/events.ts
|
|
26972
|
-
import { createHash as createHash4, randomUUID as
|
|
29503
|
+
import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
|
|
29504
|
+
|
|
29505
|
+
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
29506
|
+
import { existsSync as existsSync7 } from "fs";
|
|
29507
|
+
import { fileURLToPath } from "url";
|
|
29508
|
+
import { Worker } from "worker_threads";
|
|
26973
29509
|
|
|
26974
29510
|
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
26975
|
-
import { arch, hostname as
|
|
29511
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
26976
29512
|
function resolveInventoryContext(input) {
|
|
26977
29513
|
const host = {
|
|
26978
29514
|
objectType: "host",
|
|
26979
29515
|
// Stable-ish machine id; os/arch live in the descriptive bag (a
|
|
26980
29516
|
// harder machine id can replace this without a schema change).
|
|
26981
|
-
identityKey:
|
|
26982
|
-
title:
|
|
26983
|
-
attributes: { host_name:
|
|
29517
|
+
identityKey: hostname4(),
|
|
29518
|
+
title: hostname4(),
|
|
29519
|
+
attributes: { host_name: hostname4(), os: platform(), os_version: release(), arch: arch() }
|
|
26984
29520
|
};
|
|
26985
29521
|
const harnessAttributes = {};
|
|
26986
29522
|
if (input.harnessVersion != null) harnessAttributes.harness_version = input.harnessVersion;
|
|
@@ -27001,30 +29537,352 @@ function resolveInventoryContext(input) {
|
|
|
27001
29537
|
}
|
|
27002
29538
|
|
|
27003
29539
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
27004
|
-
import { mkdirSync as
|
|
27005
|
-
import { join as
|
|
29540
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
29541
|
+
import { join as join11 } from "path";
|
|
27006
29542
|
|
|
27007
29543
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
27008
|
-
import { readdirSync as
|
|
27009
|
-
import { basename as
|
|
29544
|
+
import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
|
|
29545
|
+
import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
|
|
27010
29546
|
|
|
27011
29547
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
27012
29548
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
27013
|
-
import { existsSync as
|
|
27014
|
-
import { basename as
|
|
29549
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
29550
|
+
import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
|
|
29551
|
+
|
|
29552
|
+
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
29553
|
+
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
29554
|
+
if (typeof v === "string" && v.trim() === "") return void 0;
|
|
29555
|
+
return v;
|
|
29556
|
+
}, external_exports.string().optional()).catch(void 0);
|
|
29557
|
+
var optionalFlag = external_exports.preprocess((v) => {
|
|
29558
|
+
if (typeof v !== "string") return false;
|
|
29559
|
+
const normalized = v.trim().toLowerCase();
|
|
29560
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
29561
|
+
}, external_exports.boolean()).catch(false);
|
|
29562
|
+
var antigravityProviderEnvShape = {
|
|
29563
|
+
GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
|
|
29564
|
+
GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
|
|
29565
|
+
};
|
|
29566
|
+
var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
|
|
29567
|
+
|
|
29568
|
+
// ../../packages/plugin-sdk/src/provider-env-codex.ts
|
|
29569
|
+
var optionalBaseUrl3 = external_exports.preprocess((v) => {
|
|
29570
|
+
if (typeof v === "string" && v.trim() === "") return void 0;
|
|
29571
|
+
return v;
|
|
29572
|
+
}, external_exports.string().optional()).catch(void 0);
|
|
29573
|
+
var codexProviderEnvShape = {
|
|
29574
|
+
OPENAI_BASE_URL: optionalBaseUrl3
|
|
29575
|
+
};
|
|
29576
|
+
var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
|
|
27015
29577
|
|
|
27016
29578
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
27017
|
-
import { randomUUID as
|
|
29579
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
27018
29580
|
|
|
27019
29581
|
// ../../packages/plugin-sdk/src/suppressions.ts
|
|
27020
29582
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27021
29583
|
|
|
27022
29584
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
27023
|
-
import { mkdirSync as
|
|
27024
|
-
import { join as
|
|
29585
|
+
import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
29586
|
+
import { join as join13 } from "path";
|
|
29587
|
+
|
|
29588
|
+
// ../../packages/plugin-sdk/src/tokenize.ts
|
|
29589
|
+
function redactedPlaceholder(category) {
|
|
29590
|
+
return `[REDACTED:${category.toUpperCase()}]`;
|
|
29591
|
+
}
|
|
29592
|
+
var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
|
|
29593
|
+
var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
29594
|
+
function groupSpans(text, findings) {
|
|
29595
|
+
const sorted = [...findings].filter((f) => f.span.start >= 0 && f.span.end <= text.length && f.span.start < f.span.end).sort((a, b) => a.span.start - b.span.start || b.span.end - a.span.end);
|
|
29596
|
+
const groups = [];
|
|
29597
|
+
for (const finding of sorted) {
|
|
29598
|
+
const last = groups[groups.length - 1];
|
|
29599
|
+
if (last && finding.span.start < last.end) {
|
|
29600
|
+
last.end = Math.max(last.end, finding.span.end);
|
|
29601
|
+
if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
|
|
29602
|
+
last.category = finding.category;
|
|
29603
|
+
last.severity = finding.severity;
|
|
29604
|
+
}
|
|
29605
|
+
delete last.finding;
|
|
29606
|
+
continue;
|
|
29607
|
+
}
|
|
29608
|
+
groups.push({
|
|
29609
|
+
start: finding.span.start,
|
|
29610
|
+
end: finding.span.end,
|
|
29611
|
+
finding,
|
|
29612
|
+
category: finding.category,
|
|
29613
|
+
severity: finding.severity
|
|
29614
|
+
});
|
|
29615
|
+
}
|
|
29616
|
+
return groups;
|
|
29617
|
+
}
|
|
29618
|
+
var NULL_RESOLVER = () => Promise.resolve(null);
|
|
29619
|
+
var SecretVaultGlue = class {
|
|
29620
|
+
#vault;
|
|
29621
|
+
revealGrantResolver;
|
|
29622
|
+
// Set only when THIS glue opened the store, so a glue over an injected vault
|
|
29623
|
+
// never closes a handle it does not own.
|
|
29624
|
+
#release;
|
|
29625
|
+
constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
|
|
29626
|
+
this.#vault = vault;
|
|
29627
|
+
this.revealGrantResolver = revealGrantResolver;
|
|
29628
|
+
this.#release = release2;
|
|
29629
|
+
}
|
|
29630
|
+
close() {
|
|
29631
|
+
const release2 = this.#release;
|
|
29632
|
+
this.#release = void 0;
|
|
29633
|
+
try {
|
|
29634
|
+
release2?.();
|
|
29635
|
+
} catch {
|
|
29636
|
+
}
|
|
29637
|
+
}
|
|
29638
|
+
async tokenizeValue(raw, meta3) {
|
|
29639
|
+
try {
|
|
29640
|
+
const result = await this.#vault.tokenize(raw, meta3);
|
|
29641
|
+
return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
|
|
29642
|
+
} catch {
|
|
29643
|
+
return redactedPlaceholder(meta3.category);
|
|
29644
|
+
}
|
|
29645
|
+
}
|
|
29646
|
+
async tokenizeText(text, opts) {
|
|
29647
|
+
try {
|
|
29648
|
+
const findings = opts?.findings ?? this.#selfScan(text);
|
|
29649
|
+
if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
29650
|
+
if (findings.length === 0) return { text, pointers: [], degraded: [] };
|
|
29651
|
+
const groups = groupSpans(text, findings);
|
|
29652
|
+
const pointers = [];
|
|
29653
|
+
const degraded = [];
|
|
29654
|
+
let out = text;
|
|
29655
|
+
for (const group of [...groups].reverse()) {
|
|
29656
|
+
const original = text.slice(group.start, group.end);
|
|
29657
|
+
const finding = group.finding;
|
|
29658
|
+
let replacement;
|
|
29659
|
+
if (finding === void 0) {
|
|
29660
|
+
replacement = redactedPlaceholder(group.category);
|
|
29661
|
+
degraded.unshift({ category: group.category });
|
|
29662
|
+
} else if (original !== finding.rawMatch) {
|
|
29663
|
+
replacement = redactedPlaceholder(group.category);
|
|
29664
|
+
degraded.unshift({ category: group.category });
|
|
29665
|
+
} else {
|
|
29666
|
+
replacement = await this.tokenizeValue(finding.rawMatch, {
|
|
29667
|
+
ruleId: finding.ruleId,
|
|
29668
|
+
category: finding.category,
|
|
29669
|
+
maskedMatch: maskMatch(finding.rawMatch)
|
|
29670
|
+
});
|
|
29671
|
+
if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
|
|
29672
|
+
else degraded.unshift({ category: finding.category });
|
|
29673
|
+
}
|
|
29674
|
+
out = out.slice(0, group.start) + replacement + out.slice(group.end);
|
|
29675
|
+
}
|
|
29676
|
+
if (opts?.sighting && pointers.length > 0) {
|
|
29677
|
+
for (const pointer of pointers) {
|
|
29678
|
+
try {
|
|
29679
|
+
const id = pointer.split(".")[1];
|
|
29680
|
+
if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
|
|
29681
|
+
} catch {
|
|
29682
|
+
}
|
|
29683
|
+
}
|
|
29684
|
+
}
|
|
29685
|
+
return { text: out, pointers, degraded };
|
|
29686
|
+
} catch {
|
|
29687
|
+
return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
29688
|
+
}
|
|
29689
|
+
}
|
|
29690
|
+
async detokenizeText(text, opts) {
|
|
29691
|
+
try {
|
|
29692
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
29693
|
+
if (matches.length === 0) return { text, revealed: 0 };
|
|
29694
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
29695
|
+
for (const match of matches) {
|
|
29696
|
+
occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
|
|
29697
|
+
}
|
|
29698
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
29699
|
+
for (const [pointer, count] of occurrences) {
|
|
29700
|
+
try {
|
|
29701
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
29702
|
+
target: "human",
|
|
29703
|
+
reason: opts.reason,
|
|
29704
|
+
pointerCount: count
|
|
29705
|
+
});
|
|
29706
|
+
resolved.set(pointer, typeof value === "string" ? value : null);
|
|
29707
|
+
} catch {
|
|
29708
|
+
resolved.set(pointer, null);
|
|
29709
|
+
}
|
|
29710
|
+
}
|
|
29711
|
+
let out = text;
|
|
29712
|
+
let revealed = 0;
|
|
29713
|
+
for (const match of [...matches].reverse()) {
|
|
29714
|
+
const value = resolved.get(match[0]);
|
|
29715
|
+
const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
|
|
29716
|
+
if (value !== null && value !== void 0) revealed += 1;
|
|
29717
|
+
out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
|
|
29718
|
+
}
|
|
29719
|
+
return { text: out, revealed };
|
|
29720
|
+
} catch {
|
|
29721
|
+
return { text, revealed: 0 };
|
|
29722
|
+
}
|
|
29723
|
+
}
|
|
29724
|
+
// Scan with the bundled packs, as the mask path does. Pointers already in the
|
|
29725
|
+
// text are blanked first so a pointer is never re-tokenized. Returns null
|
|
29726
|
+
// when the registry or the scan itself failed — the caller must then treat
|
|
29727
|
+
// the whole text as unclassifiable.
|
|
29728
|
+
#selfScan(text) {
|
|
29729
|
+
try {
|
|
29730
|
+
registerBundledPacks();
|
|
29731
|
+
const shielded = shieldPointers(text);
|
|
29732
|
+
return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
|
|
29733
|
+
} catch {
|
|
29734
|
+
return null;
|
|
29735
|
+
}
|
|
29736
|
+
}
|
|
29737
|
+
async describePointerSafe(token) {
|
|
29738
|
+
try {
|
|
29739
|
+
return await this.#vault.describePointer(token);
|
|
29740
|
+
} catch {
|
|
29741
|
+
return null;
|
|
29742
|
+
}
|
|
29743
|
+
}
|
|
29744
|
+
async probeModelPointers(text, opts) {
|
|
29745
|
+
const granted = /* @__PURE__ */ new Map();
|
|
29746
|
+
const ungranted = [];
|
|
29747
|
+
try {
|
|
29748
|
+
for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
|
|
29749
|
+
try {
|
|
29750
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29751
|
+
if (grantId === null) ungranted.push(pointer);
|
|
29752
|
+
else granted.set(pointer, grantId);
|
|
29753
|
+
} catch {
|
|
29754
|
+
ungranted.push(pointer);
|
|
29755
|
+
}
|
|
29756
|
+
}
|
|
29757
|
+
return { granted, ungranted };
|
|
29758
|
+
} catch {
|
|
29759
|
+
return { granted: /* @__PURE__ */ new Map(), ungranted };
|
|
29760
|
+
}
|
|
29761
|
+
}
|
|
29762
|
+
async substituteModelPointers(text, opts) {
|
|
29763
|
+
try {
|
|
29764
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
29765
|
+
if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29766
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
29767
|
+
for (const pointer of new Set(matches.map((m) => m[0]))) {
|
|
29768
|
+
try {
|
|
29769
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29770
|
+
if (grantId === null) {
|
|
29771
|
+
await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
|
|
29772
|
+
resolved.set(pointer, null);
|
|
29773
|
+
continue;
|
|
29774
|
+
}
|
|
29775
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
29776
|
+
target: "model",
|
|
29777
|
+
reason: "model-input",
|
|
29778
|
+
grantId
|
|
29779
|
+
});
|
|
29780
|
+
resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
|
|
29781
|
+
} catch {
|
|
29782
|
+
resolved.set(pointer, null);
|
|
29783
|
+
}
|
|
29784
|
+
}
|
|
29785
|
+
const spentGrants = /* @__PURE__ */ new Set();
|
|
29786
|
+
for (const entry of resolved.values()) {
|
|
29787
|
+
if (entry === null || spentGrants.has(entry.grantId)) continue;
|
|
29788
|
+
spentGrants.add(entry.grantId);
|
|
29789
|
+
try {
|
|
29790
|
+
await this.#vault.consumeGrant?.(entry.grantId);
|
|
29791
|
+
} catch {
|
|
29792
|
+
}
|
|
29793
|
+
}
|
|
29794
|
+
let out = text;
|
|
29795
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
29796
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
29797
|
+
for (const match of [...matches].reverse()) {
|
|
29798
|
+
const entry = resolved.get(match[0]);
|
|
29799
|
+
if (entry === null || entry === void 0) {
|
|
29800
|
+
unresolved.add(match[0]);
|
|
29801
|
+
continue;
|
|
29802
|
+
}
|
|
29803
|
+
revealed.add(match[0]);
|
|
29804
|
+
out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
|
|
29805
|
+
}
|
|
29806
|
+
return {
|
|
29807
|
+
text: out,
|
|
29808
|
+
revealed: [...revealed],
|
|
29809
|
+
unresolved: [...unresolved],
|
|
29810
|
+
grantIds: [...spentGrants]
|
|
29811
|
+
};
|
|
29812
|
+
} catch {
|
|
29813
|
+
return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29814
|
+
}
|
|
29815
|
+
}
|
|
29816
|
+
};
|
|
29817
|
+
function createVaultGlue(options) {
|
|
29818
|
+
if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
|
|
29819
|
+
const base = options?.base ?? defaultDataDir();
|
|
29820
|
+
try {
|
|
29821
|
+
const dir = dataDir(base);
|
|
29822
|
+
const db = openLocalDatabase(dir);
|
|
29823
|
+
const settings = readWorkspaceSettings(base);
|
|
29824
|
+
const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
|
|
29825
|
+
const vault = new SecretVault({
|
|
29826
|
+
repo: db.secretVault,
|
|
29827
|
+
keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
|
|
29828
|
+
// Read live so a revocation applies to the very next call, not the next
|
|
29829
|
+
// process.
|
|
29830
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
|
|
29831
|
+
// This is the one construction site that reveals to the model, so it is
|
|
29832
|
+
// the one that supplies the last gate. The decision is re-taken from the
|
|
29833
|
+
// ROW's identity at the moment of crossing, which closes the window
|
|
29834
|
+
// between resolving a grant and spending it: a grant revoked in between
|
|
29835
|
+
// refuses here.
|
|
29836
|
+
//
|
|
29837
|
+
// The re-decision is on the identity alone, never on the grant id
|
|
29838
|
+
// matching the one the resolver returned. ExceptionPolicyProvider
|
|
29839
|
+
// promises no id stability across calls — a provider deciding from
|
|
29840
|
+
// external policy may well mint a fresh id each time — so comparing ids
|
|
29841
|
+
// would silently refuse every crossing for such a provider while looking
|
|
29842
|
+
// like a security check. `allow` for this row is the whole question.
|
|
29843
|
+
verifyGrant: async (_grantId, identity) => {
|
|
29844
|
+
const decision = await provider.decideReveal(identity);
|
|
29845
|
+
return decision.allow;
|
|
29846
|
+
}
|
|
29847
|
+
});
|
|
29848
|
+
let fingerprintKey;
|
|
29849
|
+
const fingerprintKeyForWrite = () => fingerprintKey ??= loadOrCreateFingerprintKey(dir);
|
|
29850
|
+
const vaultWithSightings = {
|
|
29851
|
+
tokenize: (raw, meta3) => vault.tokenize(raw, meta3, fingerprintKeyForWrite),
|
|
29852
|
+
detokenize: (token, opts) => vault.detokenize(token, opts),
|
|
29853
|
+
describePointer: (token) => vault.describePointer(token),
|
|
29854
|
+
resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
|
|
29855
|
+
recordSighting: (pointerId, sighting) => {
|
|
29856
|
+
db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
|
|
29857
|
+
},
|
|
29858
|
+
consumeGrant: (grantId) => db.exceptions.consume(grantId)
|
|
29859
|
+
};
|
|
29860
|
+
const revealGrantResolver = async (pointer) => {
|
|
29861
|
+
try {
|
|
29862
|
+
const identity = await vault.resolvePointerIdentity(pointer);
|
|
29863
|
+
if (identity === null) return null;
|
|
29864
|
+
const decision = await provider.decideReveal(identity);
|
|
29865
|
+
return decision.allow ? decision.grantId : null;
|
|
29866
|
+
} catch {
|
|
29867
|
+
return null;
|
|
29868
|
+
}
|
|
29869
|
+
};
|
|
29870
|
+
return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
|
|
29871
|
+
db.close();
|
|
29872
|
+
});
|
|
29873
|
+
} catch {
|
|
29874
|
+
return new SecretVaultGlue(UNOPENABLE_VAULT);
|
|
29875
|
+
}
|
|
29876
|
+
}
|
|
29877
|
+
var UNOPENABLE_VAULT = {
|
|
29878
|
+
tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29879
|
+
detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29880
|
+
describePointer: () => Promise.resolve(null),
|
|
29881
|
+
resolvePointerIdentity: () => Promise.resolve(null)
|
|
29882
|
+
};
|
|
27025
29883
|
|
|
27026
29884
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
27027
|
-
import { randomUUID as
|
|
29885
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
27028
29886
|
|
|
27029
29887
|
// ../../packages/plugin-runtime/src/recorder.ts
|
|
27030
29888
|
var PLUGIN_RECORDER_BINARY = "plugin";
|
|
@@ -27063,12 +29921,12 @@ var StandaloneDataGateway = class {
|
|
|
27063
29921
|
// reconciler drops the whole pass and recovers it idempotently on the next read.
|
|
27064
29922
|
recordLlmCalls(inputs) {
|
|
27065
29923
|
if (inputs.length === 0) return Promise.resolve();
|
|
27066
|
-
return new Promise((
|
|
29924
|
+
return new Promise((resolve2, reject) => {
|
|
27067
29925
|
try {
|
|
27068
29926
|
this.db.auditEvents.runInTransaction(() => {
|
|
27069
29927
|
for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
|
|
27070
29928
|
});
|
|
27071
|
-
|
|
29929
|
+
resolve2();
|
|
27072
29930
|
} catch (err) {
|
|
27073
29931
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
27074
29932
|
}
|
|
@@ -27080,12 +29938,12 @@ var StandaloneDataGateway = class {
|
|
|
27080
29938
|
// drops the whole pass and recovers it idempotently next time.
|
|
27081
29939
|
recordToolCalls(inputs) {
|
|
27082
29940
|
if (inputs.length === 0) return Promise.resolve();
|
|
27083
|
-
return new Promise((
|
|
29941
|
+
return new Promise((resolve2, reject) => {
|
|
27084
29942
|
try {
|
|
27085
29943
|
this.db.auditEvents.runInTransaction(() => {
|
|
27086
29944
|
for (const input of inputs) this.writeToolCall(input);
|
|
27087
29945
|
});
|
|
27088
|
-
|
|
29946
|
+
resolve2();
|
|
27089
29947
|
} catch (err) {
|
|
27090
29948
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
27091
29949
|
}
|
|
@@ -27186,7 +30044,7 @@ var StandaloneDataGateway = class {
|
|
|
27186
30044
|
const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
|
|
27187
30045
|
const installed = this.installedScanRules();
|
|
27188
30046
|
const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
|
|
27189
|
-
id:
|
|
30047
|
+
id: randomUUID15(),
|
|
27190
30048
|
scope: "global",
|
|
27191
30049
|
target: { ruleId },
|
|
27192
30050
|
action,
|
|
@@ -27339,97 +30197,20 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
|
|
|
27339
30197
|
}
|
|
27340
30198
|
|
|
27341
30199
|
// ../../packages/plugin-runtime/src/handle-session-start.ts
|
|
27342
|
-
import { randomUUID as
|
|
30200
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
27343
30201
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
27344
30202
|
|
|
27345
|
-
// src/
|
|
27346
|
-
import {
|
|
27347
|
-
import {
|
|
27348
|
-
closeSync,
|
|
27349
|
-
fstatSync,
|
|
27350
|
-
mkdirSync as mkdirSync4,
|
|
27351
|
-
openSync,
|
|
27352
|
-
readFileSync as readFileSync7,
|
|
27353
|
-
readSync,
|
|
27354
|
-
writeFileSync as writeFileSync5
|
|
27355
|
-
} from "fs";
|
|
27356
|
-
import { join as join12 } from "path";
|
|
27357
|
-
function offsetsDir(dataDir2) {
|
|
27358
|
-
return join12(dataDir2, "usage-offsets");
|
|
27359
|
-
}
|
|
27360
|
-
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
27361
|
-
function safeSessionId(sessionId) {
|
|
27362
|
-
if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
|
|
27363
|
-
return sessionId;
|
|
27364
|
-
}
|
|
27365
|
-
return createHash5("sha256").update(sessionId).digest("hex");
|
|
27366
|
-
}
|
|
27367
|
-
function offsetPath(dataDir2, sessionId) {
|
|
27368
|
-
return join12(offsetsDir(dataDir2), safeSessionId(sessionId));
|
|
27369
|
-
}
|
|
27370
|
-
function readOffset(dataDir2, sessionId) {
|
|
27371
|
-
try {
|
|
27372
|
-
const raw = readFileSync7(offsetPath(dataDir2, sessionId), "utf8");
|
|
27373
|
-
const parsed = JSON.parse(raw);
|
|
27374
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
27375
|
-
const rec = parsed;
|
|
27376
|
-
const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
|
|
27377
|
-
const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
|
|
27378
|
-
return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
|
|
27379
|
-
}
|
|
27380
|
-
} catch {
|
|
27381
|
-
}
|
|
27382
|
-
return { offset: 0 };
|
|
27383
|
-
}
|
|
27384
|
-
function writeOffset(dataDir2, sessionId, value) {
|
|
27385
|
-
try {
|
|
27386
|
-
mkdirSync4(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
|
|
27387
|
-
const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
|
|
27388
|
-
writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
|
|
27389
|
-
mode: DATA_FILE_MODE
|
|
27390
|
-
});
|
|
27391
|
-
} catch {
|
|
27392
|
-
}
|
|
27393
|
-
}
|
|
27394
|
-
function readTail(transcriptPath, startOffset) {
|
|
27395
|
-
let fd;
|
|
27396
|
-
try {
|
|
27397
|
-
fd = openSync(transcriptPath, "r");
|
|
27398
|
-
} catch {
|
|
27399
|
-
return { chunk: "", nextOffset: startOffset };
|
|
27400
|
-
}
|
|
27401
|
-
try {
|
|
27402
|
-
const size = fstatSync(fd).size;
|
|
27403
|
-
const from = size < startOffset ? 0 : startOffset;
|
|
27404
|
-
if (from >= size) return { chunk: "", nextOffset: from };
|
|
27405
|
-
const length = size - from;
|
|
27406
|
-
const buf = Buffer.allocUnsafe(length);
|
|
27407
|
-
let filled = 0;
|
|
27408
|
-
while (filled < length) {
|
|
27409
|
-
const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
|
|
27410
|
-
if (bytesRead === 0) break;
|
|
27411
|
-
filled += bytesRead;
|
|
27412
|
-
}
|
|
27413
|
-
const slice = buf.subarray(0, filled);
|
|
27414
|
-
const lastNl = slice.lastIndexOf(10);
|
|
27415
|
-
if (lastNl === -1) return { chunk: "", nextOffset: from };
|
|
27416
|
-
const consumedBytes = lastNl + 1;
|
|
27417
|
-
const chunk = slice.subarray(0, consumedBytes).toString("utf8");
|
|
27418
|
-
return { chunk, nextOffset: from + consumedBytes };
|
|
27419
|
-
} catch {
|
|
27420
|
-
return { chunk: "", nextOffset: startOffset };
|
|
27421
|
-
} finally {
|
|
27422
|
-
try {
|
|
27423
|
-
closeSync(fd);
|
|
27424
|
-
} catch {
|
|
27425
|
-
}
|
|
27426
|
-
}
|
|
27427
|
-
}
|
|
30203
|
+
// src/remediation/redact.ts
|
|
30204
|
+
import { readFileSync as readFileSync10, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
30205
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
|
|
27428
30206
|
|
|
27429
30207
|
// src/history/transcripts.ts
|
|
27430
|
-
import { readdirSync as
|
|
30208
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
|
|
27431
30209
|
import { homedir as homedir3 } from "os";
|
|
27432
|
-
import { join as
|
|
30210
|
+
import { join as join14 } from "path";
|
|
30211
|
+
function transcriptsDir(home) {
|
|
30212
|
+
return join14(home ?? homedir3(), ".claude", "projects");
|
|
30213
|
+
}
|
|
27433
30214
|
function isRecord(value) {
|
|
27434
30215
|
return typeof value === "object" && value !== null;
|
|
27435
30216
|
}
|
|
@@ -27631,6 +30412,156 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
|
|
|
27631
30412
|
}
|
|
27632
30413
|
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
27633
30414
|
|
|
30415
|
+
// src/remediation/redact.ts
|
|
30416
|
+
function platformRedactionScope(home) {
|
|
30417
|
+
return { artifactRoots: [transcriptsDir(home)] };
|
|
30418
|
+
}
|
|
30419
|
+
function realPathOrNull(path) {
|
|
30420
|
+
try {
|
|
30421
|
+
return realpathSync3(path);
|
|
30422
|
+
} catch {
|
|
30423
|
+
return null;
|
|
30424
|
+
}
|
|
30425
|
+
}
|
|
30426
|
+
function isWithinRoot(realTarget, root) {
|
|
30427
|
+
const realRoot = realPathOrNull(root);
|
|
30428
|
+
if (realRoot === null) return false;
|
|
30429
|
+
const rel = relative2(realRoot, realTarget);
|
|
30430
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
30431
|
+
}
|
|
30432
|
+
function resolveRedactableArtifact(filePath, scope) {
|
|
30433
|
+
const realTarget = realPathOrNull(resolve(filePath));
|
|
30434
|
+
if (realTarget === null) return null;
|
|
30435
|
+
return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
|
|
30436
|
+
}
|
|
30437
|
+
|
|
30438
|
+
// src/history/tail.ts
|
|
30439
|
+
import { createHash as createHash5 } from "crypto";
|
|
30440
|
+
import {
|
|
30441
|
+
closeSync as closeSync2,
|
|
30442
|
+
fstatSync,
|
|
30443
|
+
mkdirSync as mkdirSync5,
|
|
30444
|
+
openSync as openSync2,
|
|
30445
|
+
readFileSync as readFileSync11,
|
|
30446
|
+
readSync,
|
|
30447
|
+
writeFileSync as writeFileSync8
|
|
30448
|
+
} from "fs";
|
|
30449
|
+
import { join as join15 } from "path";
|
|
30450
|
+
function offsetsDir(dataDir2) {
|
|
30451
|
+
return join15(dataDir2, "usage-offsets");
|
|
30452
|
+
}
|
|
30453
|
+
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
|
|
30454
|
+
function safeSessionId(sessionId) {
|
|
30455
|
+
if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
|
|
30456
|
+
return sessionId;
|
|
30457
|
+
}
|
|
30458
|
+
return createHash5("sha256").update(sessionId).digest("hex");
|
|
30459
|
+
}
|
|
30460
|
+
function offsetPath(dataDir2, sessionId) {
|
|
30461
|
+
return join15(offsetsDir(dataDir2), safeSessionId(sessionId));
|
|
30462
|
+
}
|
|
30463
|
+
function readOffset(dataDir2, sessionId) {
|
|
30464
|
+
try {
|
|
30465
|
+
const raw = readFileSync11(offsetPath(dataDir2, sessionId), "utf8");
|
|
30466
|
+
const parsed = JSON.parse(raw);
|
|
30467
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
30468
|
+
const rec = parsed;
|
|
30469
|
+
const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
|
|
30470
|
+
const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
|
|
30471
|
+
return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
|
|
30472
|
+
}
|
|
30473
|
+
} catch {
|
|
30474
|
+
}
|
|
30475
|
+
return { offset: 0 };
|
|
30476
|
+
}
|
|
30477
|
+
function writeOffset(dataDir2, sessionId, value) {
|
|
30478
|
+
try {
|
|
30479
|
+
mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
|
|
30480
|
+
const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
|
|
30481
|
+
writeFileSync8(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
|
|
30482
|
+
mode: DATA_FILE_MODE
|
|
30483
|
+
});
|
|
30484
|
+
} catch {
|
|
30485
|
+
}
|
|
30486
|
+
}
|
|
30487
|
+
function readTail(transcriptPath, startOffset) {
|
|
30488
|
+
let fd;
|
|
30489
|
+
try {
|
|
30490
|
+
fd = openSync2(transcriptPath, "r");
|
|
30491
|
+
} catch {
|
|
30492
|
+
return { chunk: "", nextOffset: startOffset };
|
|
30493
|
+
}
|
|
30494
|
+
try {
|
|
30495
|
+
const size = fstatSync(fd).size;
|
|
30496
|
+
const from = size < startOffset ? 0 : startOffset;
|
|
30497
|
+
if (from >= size) return { chunk: "", nextOffset: from };
|
|
30498
|
+
const length = size - from;
|
|
30499
|
+
const buf = Buffer.allocUnsafe(length);
|
|
30500
|
+
let filled = 0;
|
|
30501
|
+
while (filled < length) {
|
|
30502
|
+
const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
|
|
30503
|
+
if (bytesRead === 0) break;
|
|
30504
|
+
filled += bytesRead;
|
|
30505
|
+
}
|
|
30506
|
+
const slice = buf.subarray(0, filled);
|
|
30507
|
+
const lastNl = slice.lastIndexOf(10);
|
|
30508
|
+
if (lastNl === -1) return { chunk: "", nextOffset: from };
|
|
30509
|
+
const consumedBytes = lastNl + 1;
|
|
30510
|
+
const chunk = slice.subarray(0, consumedBytes).toString("utf8");
|
|
30511
|
+
return { chunk, nextOffset: from + consumedBytes };
|
|
30512
|
+
} catch {
|
|
30513
|
+
return { chunk: "", nextOffset: startOffset };
|
|
30514
|
+
} finally {
|
|
30515
|
+
try {
|
|
30516
|
+
closeSync2(fd);
|
|
30517
|
+
} catch {
|
|
30518
|
+
}
|
|
30519
|
+
}
|
|
30520
|
+
}
|
|
30521
|
+
|
|
30522
|
+
// src/history/tail-scrub.ts
|
|
30523
|
+
import { readFileSync as readFileSync12, renameSync as renameSync6, rmSync as rmSync6, statSync as statSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
30524
|
+
var DEFAULT_MAX_SCRUB_BYTES = 32 * 1024 * 1024;
|
|
30525
|
+
async function scrubTranscriptTail(filePath, deps) {
|
|
30526
|
+
try {
|
|
30527
|
+
const realPath = resolveRedactableArtifact(filePath, deps.scope);
|
|
30528
|
+
if (realPath === null) return null;
|
|
30529
|
+
const statBefore = statSync7(realPath);
|
|
30530
|
+
if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
|
|
30531
|
+
const content = readFileSync12(realPath, "utf8");
|
|
30532
|
+
const lines = content.split("\n");
|
|
30533
|
+
let rewritten = 0;
|
|
30534
|
+
for (const [i, line] of lines.entries()) {
|
|
30535
|
+
if (line === "") continue;
|
|
30536
|
+
const result = await deps.tokenizeText(line);
|
|
30537
|
+
if (result.text === line) continue;
|
|
30538
|
+
if (result.pointers.length === 0 && result.degraded.length === 0) return null;
|
|
30539
|
+
lines[i] = result.text;
|
|
30540
|
+
rewritten += 1;
|
|
30541
|
+
}
|
|
30542
|
+
if (rewritten === 0) return { rewritten: 0 };
|
|
30543
|
+
const tmpPath = `${realPath}.aka-scrub.tmp`;
|
|
30544
|
+
try {
|
|
30545
|
+
writeFileSync9(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
|
|
30546
|
+
const statNow = statSync7(realPath);
|
|
30547
|
+
if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
|
|
30548
|
+
rmSync6(tmpPath, { force: true, recursive: true });
|
|
30549
|
+
return null;
|
|
30550
|
+
}
|
|
30551
|
+
renameSync6(tmpPath, realPath);
|
|
30552
|
+
} catch {
|
|
30553
|
+
try {
|
|
30554
|
+
rmSync6(tmpPath, { force: true, recursive: true });
|
|
30555
|
+
} catch {
|
|
30556
|
+
}
|
|
30557
|
+
return null;
|
|
30558
|
+
}
|
|
30559
|
+
return { rewritten };
|
|
30560
|
+
} catch {
|
|
30561
|
+
return null;
|
|
30562
|
+
}
|
|
30563
|
+
}
|
|
30564
|
+
|
|
27634
30565
|
// src/history/usage.ts
|
|
27635
30566
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
27636
30567
|
var MAX_TARGET_LEN = 500;
|
|
@@ -27760,6 +30691,24 @@ async function reconcileSessionTail(config2, sessionId, transcriptPath) {
|
|
|
27760
30691
|
offset: nextOffset,
|
|
27761
30692
|
lastPromptId: result.lastPromptId
|
|
27762
30693
|
});
|
|
30694
|
+
if (isVaultConsentValid(config2.settings.vaultConsent)) {
|
|
30695
|
+
try {
|
|
30696
|
+
const glue = createVaultGlue();
|
|
30697
|
+
const scrubbed = await scrubTranscriptTail(transcriptPath, {
|
|
30698
|
+
tokenizeText: (text) => glue.tokenizeText(text, {
|
|
30699
|
+
sighting: { location: transcriptPath, kind: "transcript" }
|
|
30700
|
+
}),
|
|
30701
|
+
scope: platformRedactionScope()
|
|
30702
|
+
});
|
|
30703
|
+
if (scrubbed !== null && scrubbed.rewritten > 0) {
|
|
30704
|
+
writeOffset(config2.dataDir, sessionId, {
|
|
30705
|
+
offset: 0,
|
|
30706
|
+
lastPromptId: result.lastPromptId
|
|
30707
|
+
});
|
|
30708
|
+
}
|
|
30709
|
+
} catch {
|
|
30710
|
+
}
|
|
30711
|
+
}
|
|
27763
30712
|
return { llmCalls: result.llmCalls, skipped: result.skipped, toolCalls };
|
|
27764
30713
|
} finally {
|
|
27765
30714
|
await gateway.close();
|