@akasecurity/ai-tc-claude-code 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/commands/setup.md +123 -35
- package/hooks/hooks.json +11 -0
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +581 -69
- package/scripts/backfill.js +1954 -147
- package/scripts/filescan.js +602 -76
- package/scripts/firstrun.js +517 -34
- package/scripts/intro.js +176 -20
- package/scripts/message-display.js +28945 -0
- package/scripts/onboard.js +542 -30
- package/scripts/post-tool-use.js +1943 -139
- package/scripts/pre-tool-use.js +2114 -163
- package/scripts/query.js +522 -35
- package/scripts/reconcile.js +2082 -242
- package/scripts/remediate.js +1872 -104
- package/scripts/session-start.js +680 -112
- package/scripts/start-light.js +174 -18
- package/scripts/statusline.js +517 -34
- package/scripts/stop.js +185 -29
- package/scripts/user-prompt-submit.js +1910 -152
package/scripts/backfill.js
CHANGED
|
@@ -496,10 +496,10 @@ import { fileURLToPath } from "url";
|
|
|
496
496
|
|
|
497
497
|
// ../../packages/plugin-sdk/src/config.ts
|
|
498
498
|
import { existsSync as existsSync4 } from "fs";
|
|
499
|
-
import { join as
|
|
499
|
+
import { join as join7 } from "path";
|
|
500
500
|
|
|
501
501
|
// ../../packages/persistence/src/database.ts
|
|
502
|
-
import { randomUUID as
|
|
502
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
503
503
|
import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
504
504
|
import { join, sep } from "path";
|
|
505
505
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -565,6 +565,22 @@ var SQLITE_MIGRATIONS = [
|
|
|
565
565
|
{
|
|
566
566
|
tag: "0014_drop_legacy_events_findings",
|
|
567
567
|
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"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
tag: "0015_busy_vengeance",
|
|
571
|
+
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`);"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
tag: "0016_breezy_zodiak",
|
|
575
|
+
sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
tag: "0017_rainy_kat_farrell",
|
|
579
|
+
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`);"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
tag: "0018_serious_tana_nile",
|
|
583
|
+
sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
|
|
568
584
|
}
|
|
569
585
|
];
|
|
570
586
|
|
|
@@ -16224,6 +16240,7 @@ var ExceptionConditions = external_exports.object({
|
|
|
16224
16240
|
sourceTool: external_exports.string().optional(),
|
|
16225
16241
|
provider: external_exports.string().optional()
|
|
16226
16242
|
}).strict();
|
|
16243
|
+
var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
|
|
16227
16244
|
var DetectionException = external_exports.object({
|
|
16228
16245
|
id: external_exports.guid(),
|
|
16229
16246
|
ruleId: external_exports.string(),
|
|
@@ -16240,6 +16257,7 @@ var DetectionException = external_exports.object({
|
|
|
16240
16257
|
keyVersion: external_exports.number().int().positive(),
|
|
16241
16258
|
// maskMatch() preview of the approved value — never the raw value.
|
|
16242
16259
|
maskedValue: external_exports.string(),
|
|
16260
|
+
capability: ExceptionCapability.default("suppress"),
|
|
16243
16261
|
scope: ExceptionScope,
|
|
16244
16262
|
expiresAt: external_exports.iso.datetime().nullable(),
|
|
16245
16263
|
maxUses: external_exports.number().int().positive().nullable(),
|
|
@@ -16263,6 +16281,7 @@ var ExceptionBundleEntry = DetectionException.pick({
|
|
|
16263
16281
|
ruleId: true,
|
|
16264
16282
|
valueFingerprint: true,
|
|
16265
16283
|
keyVersion: true,
|
|
16284
|
+
capability: true,
|
|
16266
16285
|
expiresAt: true,
|
|
16267
16286
|
maxUses: true,
|
|
16268
16287
|
useCount: true,
|
|
@@ -17367,8 +17386,128 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
17367
17386
|
message: "At least one field must be provided"
|
|
17368
17387
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
17369
17388
|
|
|
17389
|
+
// ../../packages/schema/src/zod/vault.ts
|
|
17390
|
+
var POINTER_FORMAT_VERSION = 2;
|
|
17391
|
+
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
17392
|
+
var POINTER_TOKEN_PATTERN = new RegExp(
|
|
17393
|
+
`\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
|
|
17394
|
+
);
|
|
17395
|
+
var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
|
|
17396
|
+
function pointerTokenScanner() {
|
|
17397
|
+
return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
|
|
17398
|
+
}
|
|
17399
|
+
var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
|
|
17400
|
+
var ParsedPointer = external_exports.object({
|
|
17401
|
+
category: DetectionCategory,
|
|
17402
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17403
|
+
pointerId: external_exports.string(),
|
|
17404
|
+
tag: external_exports.string()
|
|
17405
|
+
});
|
|
17406
|
+
var VaultEntry = external_exports.object({
|
|
17407
|
+
pointerId: external_exports.string(),
|
|
17408
|
+
// The keyed HMAC of the raw value under `exception.key`, and the epoch it was
|
|
17409
|
+
// derived under. This is what a reveal-to-model grant matches on, and it rotates
|
|
17410
|
+
// independently of the vault encryption key below.
|
|
17411
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17412
|
+
fingerprintKeyVersion: external_exports.number().int().positive(),
|
|
17413
|
+
// The vault-key epoch this row's ciphertext was sealed under.
|
|
17414
|
+
keyVersion: external_exports.number().int().positive(),
|
|
17415
|
+
// Fixed at first mint and never updated: the same value detected later under a
|
|
17416
|
+
// different rule's category keeps the category it was minted with, so one
|
|
17417
|
+
// value always produces exactly one wire token.
|
|
17418
|
+
category: DetectionCategory,
|
|
17419
|
+
ruleId: external_exports.string(),
|
|
17420
|
+
// Partial-reveal preview for badges and listings. Never the raw value.
|
|
17421
|
+
maskedMatch: external_exports.string(),
|
|
17422
|
+
provider: external_exports.string().optional(),
|
|
17423
|
+
ciphertext: external_exports.string(),
|
|
17424
|
+
nonce: external_exports.string(),
|
|
17425
|
+
authTag: external_exports.string(),
|
|
17426
|
+
// How many times this value has been detected on this machine — the reuse
|
|
17427
|
+
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
17428
|
+
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
17429
|
+
firstSeen: external_exports.string(),
|
|
17430
|
+
lastSeen: external_exports.string()
|
|
17431
|
+
});
|
|
17432
|
+
var PointerDescriptor = external_exports.object({
|
|
17433
|
+
category: DetectionCategory,
|
|
17434
|
+
provider: external_exports.string().optional(),
|
|
17435
|
+
maskedMatch: external_exports.string(),
|
|
17436
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17437
|
+
firstSeen: external_exports.string(),
|
|
17438
|
+
lastSeen: external_exports.string()
|
|
17439
|
+
});
|
|
17440
|
+
var PointerIdentity = external_exports.object({
|
|
17441
|
+
ruleId: external_exports.string(),
|
|
17442
|
+
valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
17443
|
+
fingerprintKeyVersion: external_exports.number().int().positive()
|
|
17444
|
+
});
|
|
17445
|
+
var DetokenizeTarget = external_exports.enum(["human", "model"]);
|
|
17446
|
+
var VaultDerefReason = external_exports.enum([
|
|
17447
|
+
"display",
|
|
17448
|
+
"explicit-reveal",
|
|
17449
|
+
"view-render",
|
|
17450
|
+
"model-input",
|
|
17451
|
+
"remediation",
|
|
17452
|
+
"purge"
|
|
17453
|
+
]);
|
|
17454
|
+
var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
|
|
17455
|
+
var BATCHED_DEREF_REASONS = ["display", "view-render"];
|
|
17456
|
+
function isBatchedDerefReason(reason) {
|
|
17457
|
+
return BATCHED_DEREF_REASONS.includes(reason);
|
|
17458
|
+
}
|
|
17459
|
+
var VaultDeref = external_exports.object({
|
|
17460
|
+
id: external_exports.guid(),
|
|
17461
|
+
pointerId: external_exports.string(),
|
|
17462
|
+
at: external_exports.string(),
|
|
17463
|
+
target: DetokenizeTarget,
|
|
17464
|
+
reason: VaultDerefReason,
|
|
17465
|
+
outcome: VaultDerefOutcome,
|
|
17466
|
+
// Present only on a model-target crossing that a reveal grant authorized.
|
|
17467
|
+
grantId: external_exports.string().optional(),
|
|
17468
|
+
// How many pointers ONE batched render resolved. 1 for unbatched rows. Named
|
|
17469
|
+
// apart from VaultEntry.occurrenceCount, which counts detections of a value.
|
|
17470
|
+
pointerCount: external_exports.number().int().positive().default(1)
|
|
17471
|
+
});
|
|
17472
|
+
var VaultSightingKind = external_exports.enum([
|
|
17473
|
+
"prompt",
|
|
17474
|
+
"tool-input",
|
|
17475
|
+
"tool-output",
|
|
17476
|
+
"file",
|
|
17477
|
+
"transcript"
|
|
17478
|
+
]);
|
|
17479
|
+
var VaultSighting = external_exports.object({
|
|
17480
|
+
location: external_exports.string(),
|
|
17481
|
+
kind: VaultSightingKind,
|
|
17482
|
+
firstSeen: external_exports.string(),
|
|
17483
|
+
lastSeen: external_exports.string()
|
|
17484
|
+
});
|
|
17485
|
+
var VaultInventoryEntry = external_exports.object({
|
|
17486
|
+
pointerId: external_exports.string(),
|
|
17487
|
+
category: DetectionCategory,
|
|
17488
|
+
provider: external_exports.string().optional(),
|
|
17489
|
+
maskedMatch: external_exports.string(),
|
|
17490
|
+
occurrences: external_exports.number().int().nonnegative(),
|
|
17491
|
+
firstSeen: external_exports.string(),
|
|
17492
|
+
lastSeen: external_exports.string(),
|
|
17493
|
+
// The active reveal-to-model grant covering this value, when one exists —
|
|
17494
|
+
// the inventory badges it, the row links to revocation.
|
|
17495
|
+
revealGrantId: external_exports.string().nullable(),
|
|
17496
|
+
sightings: external_exports.array(VaultSighting)
|
|
17497
|
+
});
|
|
17498
|
+
var VaultKeyCustody = external_exports.string();
|
|
17499
|
+
var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
|
|
17500
|
+
var VAULT_CONSENT_VERSION = 1;
|
|
17501
|
+
var VaultConsent = external_exports.object({
|
|
17502
|
+
acknowledgedAt: external_exports.iso.datetime(),
|
|
17503
|
+
version: external_exports.number().int().positive()
|
|
17504
|
+
});
|
|
17505
|
+
function isVaultConsentValid(consent) {
|
|
17506
|
+
return consent?.version === VAULT_CONSENT_VERSION;
|
|
17507
|
+
}
|
|
17508
|
+
|
|
17370
17509
|
// ../../packages/schema/src/zod/local.ts
|
|
17371
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
17510
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
|
|
17372
17511
|
var RunMode = external_exports.enum(["standalone"]);
|
|
17373
17512
|
var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
|
|
17374
17513
|
var HistoricalAccess = external_exports.enum(["full", "session-only"]);
|
|
@@ -17390,6 +17529,16 @@ var WorkspaceSettings = external_exports.object({
|
|
|
17390
17529
|
// In-place egress extraction on the scan paths; disable to stop all Data
|
|
17391
17530
|
// Shares writes.
|
|
17392
17531
|
dataSharesInPlace: external_exports.boolean().default(true),
|
|
17532
|
+
// Consent to keep a RECOVERABLE encrypted copy of detected values in the local
|
|
17533
|
+
// vault, instead of destroying them. Absent by default: this is a custody
|
|
17534
|
+
// change from one-way redaction, so it is never an assumed grant on upgrade.
|
|
17535
|
+
// Revoking stops future vaulting; it does not erase what is already stored —
|
|
17536
|
+
// purging the vault is the eraser.
|
|
17537
|
+
vaultConsent: VaultConsent.optional(),
|
|
17538
|
+
// Where the vault master key lives.
|
|
17539
|
+
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
17540
|
+
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
17541
|
+
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
17393
17542
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
17394
17543
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
17395
17544
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -19804,6 +19953,9 @@ var AmbiguousExceptionIdError = class extends Error {
|
|
|
19804
19953
|
var ACTIVE_PREDICATE = `revoked_at IS NULL
|
|
19805
19954
|
AND (expires_at IS NULL OR expires_at > :now)
|
|
19806
19955
|
AND (max_uses IS NULL OR use_count < max_uses)`;
|
|
19956
|
+
var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
|
|
19957
|
+
AND conditions IS NULL
|
|
19958
|
+
AND ${ACTIVE_PREDICATE}`;
|
|
19807
19959
|
var SqliteExceptionsRepository = class {
|
|
19808
19960
|
constructor(db) {
|
|
19809
19961
|
this.db = db;
|
|
@@ -19895,11 +20047,11 @@ var SqliteExceptionsRepository = class {
|
|
|
19895
20047
|
this.db.prepare(
|
|
19896
20048
|
`INSERT INTO exceptions (
|
|
19897
20049
|
id, rule_id, category, value_fingerprint, key_version, masked_value,
|
|
19898
|
-
scope, expires_at, max_uses, use_count, last_used_at,
|
|
19899
|
-
conditions, created_by, created_via, created_at, updated_at
|
|
20050
|
+
capability, scope, expires_at, max_uses, use_count, last_used_at,
|
|
20051
|
+
justification, conditions, created_by, created_via, created_at, updated_at
|
|
19900
20052
|
) VALUES (
|
|
19901
20053
|
:id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
|
|
19902
|
-
:scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
20054
|
+
:capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
|
|
19903
20055
|
:conditions, :createdBy, :createdVia, :now, :now
|
|
19904
20056
|
)`
|
|
19905
20057
|
).run({
|
|
@@ -19909,6 +20061,7 @@ var SqliteExceptionsRepository = class {
|
|
|
19909
20061
|
valueFingerprint: input.valueFingerprint,
|
|
19910
20062
|
keyVersion: input.keyVersion,
|
|
19911
20063
|
maskedValue: input.maskedValue,
|
|
20064
|
+
capability: input.capability ?? "suppress",
|
|
19912
20065
|
scope: input.scope,
|
|
19913
20066
|
expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
|
|
19914
20067
|
maxUses: input.maxUses,
|
|
@@ -20002,6 +20155,7 @@ var SqliteExceptionsRepository = class {
|
|
|
20002
20155
|
ruleId: row.rule_id,
|
|
20003
20156
|
valueFingerprint: row.value_fingerprint,
|
|
20004
20157
|
keyVersion: row.key_version,
|
|
20158
|
+
capability: row.capability,
|
|
20005
20159
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20006
20160
|
maxUses: row.max_uses,
|
|
20007
20161
|
useCount: row.use_count,
|
|
@@ -20056,6 +20210,35 @@ var SqliteExceptionsRepository = class {
|
|
|
20056
20210
|
}))
|
|
20057
20211
|
);
|
|
20058
20212
|
}
|
|
20213
|
+
/**
|
|
20214
|
+
* The active reveal-to-model grant for a vaulted value's identity, or null.
|
|
20215
|
+
* Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
|
|
20216
|
+
* suppression uses — plus the capability: a suppression grant must never
|
|
20217
|
+
* authorize a reveal. Read-only: the caller does NOT consume here, because a
|
|
20218
|
+
* revealed value re-enters the detection scan immediately afterward and the
|
|
20219
|
+
* suppression match there claims the use — one crossing, one use.
|
|
20220
|
+
*
|
|
20221
|
+
* A grant with `conditions` NEVER matches here: the reveal path does not yet
|
|
20222
|
+
* evaluate conditions, and a narrowing clause that is ignored would WIDEN the
|
|
20223
|
+
* grant instead. Fail closed until reveal-side condition evaluation exists.
|
|
20224
|
+
*/
|
|
20225
|
+
activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
|
|
20226
|
+
try {
|
|
20227
|
+
const row = getRow(
|
|
20228
|
+
this.db.prepare(
|
|
20229
|
+
`SELECT id FROM exceptions
|
|
20230
|
+
WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
|
|
20231
|
+
AND key_version = :keyVersion
|
|
20232
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
20233
|
+
LIMIT 1`
|
|
20234
|
+
),
|
|
20235
|
+
{ ruleId, valueFingerprint, keyVersion, now }
|
|
20236
|
+
);
|
|
20237
|
+
return Promise.resolve(row ?? null);
|
|
20238
|
+
} catch (err) {
|
|
20239
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
20240
|
+
}
|
|
20241
|
+
}
|
|
20059
20242
|
/**
|
|
20060
20243
|
* Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
|
|
20061
20244
|
* exhausted) whose last transition is older than the retention window.
|
|
@@ -20083,6 +20266,7 @@ function parseExceptionRow(row) {
|
|
|
20083
20266
|
valueFingerprint: row.value_fingerprint,
|
|
20084
20267
|
keyVersion: row.key_version,
|
|
20085
20268
|
maskedValue: row.masked_value,
|
|
20269
|
+
capability: row.capability,
|
|
20086
20270
|
scope: row.scope,
|
|
20087
20271
|
expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
|
|
20088
20272
|
maxUses: row.max_uses,
|
|
@@ -22248,6 +22432,287 @@ var SqliteScanLedgerRepository = class {
|
|
|
22248
22432
|
}
|
|
22249
22433
|
};
|
|
22250
22434
|
|
|
22435
|
+
// ../../packages/persistence/src/repositories/secret-vault.ts
|
|
22436
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
22437
|
+
var SELECT_COLUMNS = `
|
|
22438
|
+
pointer_id AS pointerId,
|
|
22439
|
+
value_fingerprint AS valueFingerprint,
|
|
22440
|
+
fingerprint_key_version AS fingerprintKeyVersion,
|
|
22441
|
+
key_version AS keyVersion,
|
|
22442
|
+
format_version AS formatVersion,
|
|
22443
|
+
category,
|
|
22444
|
+
rule_id AS ruleId,
|
|
22445
|
+
masked_match AS maskedMatch,
|
|
22446
|
+
provider,
|
|
22447
|
+
ciphertext,
|
|
22448
|
+
nonce,
|
|
22449
|
+
auth_tag AS authTag,
|
|
22450
|
+
occurrence_count AS occurrenceCount,
|
|
22451
|
+
first_seen AS firstSeen,
|
|
22452
|
+
last_seen AS lastSeen`;
|
|
22453
|
+
function toRow(raw) {
|
|
22454
|
+
const { provider, ...rest } = raw;
|
|
22455
|
+
return provider === null ? rest : { ...rest, provider };
|
|
22456
|
+
}
|
|
22457
|
+
var SqliteSecretVaultRepository = class {
|
|
22458
|
+
constructor(db) {
|
|
22459
|
+
this.db = db;
|
|
22460
|
+
this.insertStmt = db.prepare(
|
|
22461
|
+
`INSERT INTO secret_vault (
|
|
22462
|
+
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
22463
|
+
format_version, category, rule_id, masked_match, provider,
|
|
22464
|
+
ciphertext, nonce, auth_tag,
|
|
22465
|
+
occurrence_count, first_seen, last_seen
|
|
22466
|
+
) VALUES (
|
|
22467
|
+
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
22468
|
+
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
22469
|
+
:ciphertext, :nonce, :authTag,
|
|
22470
|
+
1, :now, :now
|
|
22471
|
+
)`
|
|
22472
|
+
);
|
|
22473
|
+
this.bumpStmt = db.prepare(
|
|
22474
|
+
`UPDATE secret_vault
|
|
22475
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
22476
|
+
WHERE value_fingerprint = :valueFingerprint`
|
|
22477
|
+
);
|
|
22478
|
+
this.byPointerStmt = db.prepare(
|
|
22479
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
|
|
22480
|
+
);
|
|
22481
|
+
this.byFingerprintStmt = db.prepare(
|
|
22482
|
+
`SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
|
|
22483
|
+
);
|
|
22484
|
+
this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
|
|
22485
|
+
this.replaceCiphertextStmt = db.prepare(
|
|
22486
|
+
`UPDATE secret_vault
|
|
22487
|
+
SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
|
|
22488
|
+
WHERE pointer_id = :pointerId`
|
|
22489
|
+
);
|
|
22490
|
+
this.refreshFingerprintStmt = db.prepare(
|
|
22491
|
+
`UPDATE secret_vault
|
|
22492
|
+
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
22493
|
+
WHERE pointer_id = :pointerId`
|
|
22494
|
+
);
|
|
22495
|
+
this.derefStmt = db.prepare(
|
|
22496
|
+
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
22497
|
+
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
22498
|
+
);
|
|
22499
|
+
}
|
|
22500
|
+
db;
|
|
22501
|
+
insertStmt;
|
|
22502
|
+
bumpStmt;
|
|
22503
|
+
byPointerStmt;
|
|
22504
|
+
byFingerprintStmt;
|
|
22505
|
+
listStmt;
|
|
22506
|
+
replaceCiphertextStmt;
|
|
22507
|
+
refreshFingerprintStmt;
|
|
22508
|
+
derefStmt;
|
|
22509
|
+
/**
|
|
22510
|
+
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
22511
|
+
* `valueFingerprint`, never on the caller's pointer id: a value seen again
|
|
22512
|
+
* bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
|
|
22513
|
+
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
22514
|
+
* wire token. `minted` is true only when this call created the row.
|
|
22515
|
+
*
|
|
22516
|
+
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
22517
|
+
* writers cannot both decide they are minting.
|
|
22518
|
+
*/
|
|
22519
|
+
upsert(input, now) {
|
|
22520
|
+
let minted = false;
|
|
22521
|
+
withTransaction(
|
|
22522
|
+
this.db,
|
|
22523
|
+
() => {
|
|
22524
|
+
const existing = getRow(this.byFingerprintStmt, {
|
|
22525
|
+
valueFingerprint: input.valueFingerprint
|
|
22526
|
+
});
|
|
22527
|
+
if (existing === void 0) {
|
|
22528
|
+
this.insertStmt.run(
|
|
22529
|
+
bindParams({
|
|
22530
|
+
pointerId: input.pointerId,
|
|
22531
|
+
valueFingerprint: input.valueFingerprint,
|
|
22532
|
+
fingerprintKeyVersion: input.fingerprintKeyVersion,
|
|
22533
|
+
keyVersion: input.keyVersion,
|
|
22534
|
+
formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
|
|
22535
|
+
category: input.category,
|
|
22536
|
+
ruleId: input.ruleId,
|
|
22537
|
+
maskedMatch: input.maskedMatch,
|
|
22538
|
+
provider: input.provider,
|
|
22539
|
+
ciphertext: input.ciphertext,
|
|
22540
|
+
nonce: input.nonce,
|
|
22541
|
+
authTag: input.authTag,
|
|
22542
|
+
now
|
|
22543
|
+
})
|
|
22544
|
+
);
|
|
22545
|
+
minted = true;
|
|
22546
|
+
return;
|
|
22547
|
+
}
|
|
22548
|
+
this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
|
|
22549
|
+
},
|
|
22550
|
+
"IMMEDIATE"
|
|
22551
|
+
);
|
|
22552
|
+
const row = getRow(this.byFingerprintStmt, {
|
|
22553
|
+
valueFingerprint: input.valueFingerprint
|
|
22554
|
+
});
|
|
22555
|
+
if (row === void 0) throw new Error("vault: row vanished immediately after write");
|
|
22556
|
+
return { row: toRow(row), minted };
|
|
22557
|
+
}
|
|
22558
|
+
byPointerId(pointerId) {
|
|
22559
|
+
const raw = getRow(this.byPointerStmt, { pointerId });
|
|
22560
|
+
return raw === void 0 ? null : toRow(raw);
|
|
22561
|
+
}
|
|
22562
|
+
byValueFingerprint(fingerprint) {
|
|
22563
|
+
const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
|
|
22564
|
+
return raw === void 0 ? null : toRow(raw);
|
|
22565
|
+
}
|
|
22566
|
+
/** Append one audit row. Carries no raw value and no ciphertext, by shape. */
|
|
22567
|
+
recordDeref(entry) {
|
|
22568
|
+
this.derefStmt.run(
|
|
22569
|
+
bindParams({
|
|
22570
|
+
id: entry.id,
|
|
22571
|
+
pointerId: entry.pointerId,
|
|
22572
|
+
at: entry.at,
|
|
22573
|
+
target: entry.target,
|
|
22574
|
+
reason: entry.reason,
|
|
22575
|
+
outcome: entry.outcome,
|
|
22576
|
+
grantId: entry.grantId,
|
|
22577
|
+
pointerCount: entry.pointerCount ?? 1
|
|
22578
|
+
})
|
|
22579
|
+
);
|
|
22580
|
+
}
|
|
22581
|
+
listAll() {
|
|
22582
|
+
return allRows(this.listStmt).map(toRow);
|
|
22583
|
+
}
|
|
22584
|
+
/** Re-seal an entry under a new key epoch, leaving its identity untouched. */
|
|
22585
|
+
replaceCiphertext(pointerId, next) {
|
|
22586
|
+
this.replaceCiphertextStmt.run({ pointerId, ...next });
|
|
22587
|
+
}
|
|
22588
|
+
/** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
|
|
22589
|
+
refreshFingerprint(pointerId, next) {
|
|
22590
|
+
this.refreshFingerprintStmt.run({ pointerId, ...next });
|
|
22591
|
+
}
|
|
22592
|
+
/**
|
|
22593
|
+
* Destroy every vaulted value and report how many were destroyed. The deref
|
|
22594
|
+
* audit is left alone on purpose — see the table note above.
|
|
22595
|
+
*/
|
|
22596
|
+
purgeAll() {
|
|
22597
|
+
let destroyed = 0;
|
|
22598
|
+
withTransaction(
|
|
22599
|
+
this.db,
|
|
22600
|
+
() => {
|
|
22601
|
+
destroyed = this.countEntries();
|
|
22602
|
+
this.db.exec("DELETE FROM secret_vault");
|
|
22603
|
+
},
|
|
22604
|
+
"IMMEDIATE"
|
|
22605
|
+
);
|
|
22606
|
+
return destroyed;
|
|
22607
|
+
}
|
|
22608
|
+
/**
|
|
22609
|
+
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
22610
|
+
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
22611
|
+
* on hook paths — a failure must never affect the rewrite that triggered it,
|
|
22612
|
+
* so callers wrap this, not the other way around.
|
|
22613
|
+
*/
|
|
22614
|
+
recordSighting(entry, now) {
|
|
22615
|
+
this.db.prepare(
|
|
22616
|
+
`INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
|
|
22617
|
+
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
22618
|
+
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
22619
|
+
).run({
|
|
22620
|
+
id: randomUUID7(),
|
|
22621
|
+
pointerId: entry.pointerId,
|
|
22622
|
+
location: entry.location,
|
|
22623
|
+
kind: entry.kind,
|
|
22624
|
+
now
|
|
22625
|
+
});
|
|
22626
|
+
}
|
|
22627
|
+
listSightings(pointerId) {
|
|
22628
|
+
const rows = allRows(
|
|
22629
|
+
this.db.prepare(
|
|
22630
|
+
`SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
|
|
22631
|
+
WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
|
|
22632
|
+
),
|
|
22633
|
+
{ pointerId }
|
|
22634
|
+
);
|
|
22635
|
+
return rows.map((r) => ({
|
|
22636
|
+
location: r.location,
|
|
22637
|
+
kind: r.kind,
|
|
22638
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
22639
|
+
lastSeen: new Date(r.last_seen).toISOString()
|
|
22640
|
+
}));
|
|
22641
|
+
}
|
|
22642
|
+
/**
|
|
22643
|
+
* The dashboard inventory: every vaulted value's descriptor data joined with
|
|
22644
|
+
* its sightings and the active reveal-to-model grant when one exists.
|
|
22645
|
+
* Raw-free by construction — neither the fingerprint nor the ciphertext
|
|
22646
|
+
* columns are selected.
|
|
22647
|
+
*/
|
|
22648
|
+
listInventory(now = Date.now()) {
|
|
22649
|
+
const rows = allRows(
|
|
22650
|
+
this.db.prepare(
|
|
22651
|
+
`SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
|
|
22652
|
+
v.occurrence_count, v.first_seen, v.last_seen,
|
|
22653
|
+
(SELECT e.id FROM exceptions e
|
|
22654
|
+
WHERE e.rule_id = v.rule_id
|
|
22655
|
+
AND e.value_fingerprint = v.value_fingerprint
|
|
22656
|
+
AND e.key_version = v.fingerprint_key_version
|
|
22657
|
+
AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
|
|
22658
|
+
LIMIT 1) AS grant_id
|
|
22659
|
+
FROM secret_vault v
|
|
22660
|
+
ORDER BY v.last_seen DESC`
|
|
22661
|
+
),
|
|
22662
|
+
{ now }
|
|
22663
|
+
);
|
|
22664
|
+
return rows.map((r) => ({
|
|
22665
|
+
pointerId: r.pointer_id,
|
|
22666
|
+
category: r.category,
|
|
22667
|
+
...r.provider === null ? {} : { provider: r.provider },
|
|
22668
|
+
maskedMatch: r.masked_match,
|
|
22669
|
+
occurrences: r.occurrence_count,
|
|
22670
|
+
firstSeen: new Date(r.first_seen).toISOString(),
|
|
22671
|
+
lastSeen: new Date(r.last_seen).toISOString(),
|
|
22672
|
+
revealGrantId: r.grant_id,
|
|
22673
|
+
sightings: this.listSightings(r.pointer_id)
|
|
22674
|
+
}));
|
|
22675
|
+
}
|
|
22676
|
+
/**
|
|
22677
|
+
* The de-reference trail, newest first. By default the batched, high-volume
|
|
22678
|
+
* reasons (display, view-render) are hidden and counted instead — the rows
|
|
22679
|
+
* that matter as a signal are the model crossings, and burying them under
|
|
22680
|
+
* render noise would defeat the audit's purpose.
|
|
22681
|
+
*/
|
|
22682
|
+
listDerefs(opts) {
|
|
22683
|
+
const limit = opts?.limit ?? 200;
|
|
22684
|
+
const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
|
|
22685
|
+
const rows = allRows(
|
|
22686
|
+
this.db.prepare(
|
|
22687
|
+
`SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
|
|
22688
|
+
FROM secret_vault_deref ${where}
|
|
22689
|
+
ORDER BY at DESC, rowid DESC LIMIT :limit`
|
|
22690
|
+
),
|
|
22691
|
+
{ limit }
|
|
22692
|
+
);
|
|
22693
|
+
const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
|
|
22694
|
+
this.db,
|
|
22695
|
+
`SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
|
|
22696
|
+
);
|
|
22697
|
+
return {
|
|
22698
|
+
rows: rows.map((r) => ({
|
|
22699
|
+
id: r.id,
|
|
22700
|
+
pointerId: r.pointer_id,
|
|
22701
|
+
at: new Date(r.at).toISOString(),
|
|
22702
|
+
target: r.target,
|
|
22703
|
+
reason: r.reason,
|
|
22704
|
+
outcome: r.outcome,
|
|
22705
|
+
...r.grant_id === null ? {} : { grantId: r.grant_id },
|
|
22706
|
+
pointerCount: r.pointer_count
|
|
22707
|
+
})),
|
|
22708
|
+
hiddenBatched
|
|
22709
|
+
};
|
|
22710
|
+
}
|
|
22711
|
+
countEntries() {
|
|
22712
|
+
return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
|
|
22713
|
+
}
|
|
22714
|
+
};
|
|
22715
|
+
|
|
22251
22716
|
// ../../packages/persistence/src/repositories/security.ts
|
|
22252
22717
|
var DAY_MS4 = 864e5;
|
|
22253
22718
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
@@ -22593,7 +23058,7 @@ var SqliteSecurityRepository = class {
|
|
|
22593
23058
|
};
|
|
22594
23059
|
|
|
22595
23060
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
22596
|
-
import { randomUUID as
|
|
23061
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
22597
23062
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
22598
23063
|
var IN_CHUNK = 500;
|
|
22599
23064
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -22849,7 +23314,7 @@ var SqliteSharesRepository = class {
|
|
|
22849
23314
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
22850
23315
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
22851
23316
|
).run({
|
|
22852
|
-
id:
|
|
23317
|
+
id: randomUUID8(),
|
|
22853
23318
|
destinationId,
|
|
22854
23319
|
host: dest.host,
|
|
22855
23320
|
decision,
|
|
@@ -22998,7 +23463,7 @@ var SqliteSharesRepository = class {
|
|
|
22998
23463
|
let destinationId = destIds.get(hit.host);
|
|
22999
23464
|
if (destinationId === void 0) {
|
|
23000
23465
|
destStmt.run({
|
|
23001
|
-
id:
|
|
23466
|
+
id: randomUUID8(),
|
|
23002
23467
|
kind: hit.kind,
|
|
23003
23468
|
name: hit.name,
|
|
23004
23469
|
host: hit.host,
|
|
@@ -23014,7 +23479,7 @@ var SqliteSharesRepository = class {
|
|
|
23014
23479
|
let endpointId = endpointIds.get(endpointKey);
|
|
23015
23480
|
if (endpointId === void 0) {
|
|
23016
23481
|
endpointStmt.run({
|
|
23017
|
-
id:
|
|
23482
|
+
id: randomUUID8(),
|
|
23018
23483
|
destinationId,
|
|
23019
23484
|
method: hit.method,
|
|
23020
23485
|
transport: hit.transport,
|
|
@@ -23027,7 +23492,7 @@ var SqliteSharesRepository = class {
|
|
|
23027
23492
|
endpointIds.set(endpointKey, endpointId);
|
|
23028
23493
|
}
|
|
23029
23494
|
siteStmt.run({
|
|
23030
|
-
id:
|
|
23495
|
+
id: randomUUID8(),
|
|
23031
23496
|
endpointId,
|
|
23032
23497
|
project: input.project,
|
|
23033
23498
|
projectKey: input.projectKey,
|
|
@@ -23443,6 +23908,7 @@ function openAndInitialize(file2) {
|
|
|
23443
23908
|
policies,
|
|
23444
23909
|
installedPacks,
|
|
23445
23910
|
scanLedger: new SqliteScanLedgerRepository(db),
|
|
23911
|
+
secretVault: new SqliteSecretVaultRepository(db),
|
|
23446
23912
|
exceptions: new SqliteExceptionsRepository(db),
|
|
23447
23913
|
resolutions: new SqliteResolutionsRepository(db),
|
|
23448
23914
|
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
@@ -23478,6 +23944,7 @@ function openLocalDatabase(dir) {
|
|
|
23478
23944
|
policies,
|
|
23479
23945
|
installedPacks,
|
|
23480
23946
|
scanLedger,
|
|
23947
|
+
secretVault,
|
|
23481
23948
|
exceptions,
|
|
23482
23949
|
resolutions,
|
|
23483
23950
|
ruleProbeCache,
|
|
@@ -23586,7 +24053,7 @@ function openLocalDatabase(dir) {
|
|
|
23586
24053
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
23587
24054
|
if (!definitionId) continue;
|
|
23588
24055
|
inspectionFindings.insertFinding({
|
|
23589
|
-
id:
|
|
24056
|
+
id: randomUUID9(),
|
|
23590
24057
|
auditEventId: record2.scanEvent.id,
|
|
23591
24058
|
inspectionDefinitionId: definitionId,
|
|
23592
24059
|
span: finding.span,
|
|
@@ -23663,6 +24130,7 @@ function openLocalDatabase(dir) {
|
|
|
23663
24130
|
policies,
|
|
23664
24131
|
installedPacks,
|
|
23665
24132
|
scanLedger,
|
|
24133
|
+
secretVault,
|
|
23666
24134
|
exceptions,
|
|
23667
24135
|
resolutions,
|
|
23668
24136
|
ruleProbeCache,
|
|
@@ -23695,6 +24163,26 @@ function openLocalDatabase(dir) {
|
|
|
23695
24163
|
};
|
|
23696
24164
|
}
|
|
23697
24165
|
|
|
24166
|
+
// ../../packages/persistence/src/exception-policy.ts
|
|
24167
|
+
var UserGrantPolicyProvider = class {
|
|
24168
|
+
#exceptions;
|
|
24169
|
+
constructor(exceptions) {
|
|
24170
|
+
this.#exceptions = exceptions;
|
|
24171
|
+
}
|
|
24172
|
+
async decideReveal(identity) {
|
|
24173
|
+
try {
|
|
24174
|
+
const grant = await this.#exceptions.activeRevealGrant(
|
|
24175
|
+
identity.ruleId,
|
|
24176
|
+
identity.valueFingerprint,
|
|
24177
|
+
identity.fingerprintKeyVersion
|
|
24178
|
+
);
|
|
24179
|
+
return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
|
|
24180
|
+
} catch {
|
|
24181
|
+
return { allow: false };
|
|
24182
|
+
}
|
|
24183
|
+
}
|
|
24184
|
+
};
|
|
24185
|
+
|
|
23698
24186
|
// ../../packages/persistence/src/finding-key.ts
|
|
23699
24187
|
import { createHash as createHash3 } from "crypto";
|
|
23700
24188
|
function normalizeFilePath(filePath) {
|
|
@@ -23733,7 +24221,11 @@ function parseKeyFile(raw) {
|
|
|
23733
24221
|
}
|
|
23734
24222
|
return { version: version2, material: bytes };
|
|
23735
24223
|
}
|
|
23736
|
-
var
|
|
24224
|
+
var KEY_VERSION_COLUMNS = {
|
|
24225
|
+
exceptions: "key_version",
|
|
24226
|
+
blocked_detections: "key_version",
|
|
24227
|
+
secret_vault: "fingerprint_key_version"
|
|
24228
|
+
};
|
|
23737
24229
|
var SQLITE_ERROR = 1;
|
|
23738
24230
|
var FLOOR_BUSY_TIMEOUT_MS = 250;
|
|
23739
24231
|
var FloorUnreadableError = class extends Error {
|
|
@@ -23754,10 +24246,10 @@ function storedKeyVersionFloor(dataDir2) {
|
|
|
23754
24246
|
db = new DatabaseSync2(file2, { readOnly: true });
|
|
23755
24247
|
db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
|
|
23756
24248
|
let floor = 0;
|
|
23757
|
-
for (const table of
|
|
24249
|
+
for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
|
|
23758
24250
|
try {
|
|
23759
24251
|
const row = getRow(
|
|
23760
|
-
db.prepare(`SELECT MAX(
|
|
24252
|
+
db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
|
|
23761
24253
|
);
|
|
23762
24254
|
floor = Math.max(floor, row?.v ?? 0);
|
|
23763
24255
|
} catch (err) {
|
|
@@ -23823,6 +24315,9 @@ function dataDir(base = defaultDataDir()) {
|
|
|
23823
24315
|
function dbPath(base = defaultDataDir()) {
|
|
23824
24316
|
return join3(dataDir(base), "aka.db");
|
|
23825
24317
|
}
|
|
24318
|
+
function keysDir(base = defaultDataDir()) {
|
|
24319
|
+
return join3(base, "keys");
|
|
24320
|
+
}
|
|
23826
24321
|
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
23827
24322
|
ensureDataDirSync(dir);
|
|
23828
24323
|
}
|
|
@@ -23864,88 +24359,949 @@ function readJson(file2) {
|
|
|
23864
24359
|
return parseJsonObject(text) ?? null;
|
|
23865
24360
|
}
|
|
23866
24361
|
|
|
23867
|
-
// ../../packages/persistence/src/
|
|
23868
|
-
import {
|
|
23869
|
-
|
|
23870
|
-
|
|
23871
|
-
|
|
23872
|
-
|
|
23873
|
-
|
|
23874
|
-
|
|
23875
|
-
|
|
23876
|
-
|
|
23877
|
-
|
|
23878
|
-
|
|
24362
|
+
// ../../packages/persistence/src/vault/crypto.ts
|
|
24363
|
+
import {
|
|
24364
|
+
createCipheriv,
|
|
24365
|
+
createDecipheriv,
|
|
24366
|
+
createHmac as createHmac2,
|
|
24367
|
+
hkdfSync,
|
|
24368
|
+
timingSafeEqual
|
|
24369
|
+
} from "crypto";
|
|
24370
|
+
var POINTER_ID_BYTES = 16;
|
|
24371
|
+
var NONCE_BYTES = 12;
|
|
24372
|
+
var TAG_BYTES = 10;
|
|
24373
|
+
var SUBKEY_BYTES = 32;
|
|
24374
|
+
var HKDF_INFO_ENC = "aka:vault:enc:v1";
|
|
24375
|
+
var HKDF_INFO_SIGN = "aka:vault:sign:v1";
|
|
24376
|
+
var HKDF_SALT = "aka:vault:v1";
|
|
24377
|
+
var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
24378
|
+
function base32Encode(bytes) {
|
|
24379
|
+
let out = "";
|
|
24380
|
+
let buffer = 0;
|
|
24381
|
+
let bits = 0;
|
|
24382
|
+
for (const byte of bytes) {
|
|
24383
|
+
buffer = buffer << 8 | byte;
|
|
24384
|
+
bits += 8;
|
|
24385
|
+
while (bits >= 5) {
|
|
24386
|
+
out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
|
|
24387
|
+
bits -= 5;
|
|
24388
|
+
}
|
|
24389
|
+
}
|
|
24390
|
+
if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
|
|
24391
|
+
return out;
|
|
23879
24392
|
}
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
23888
|
-
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
|
|
23892
|
-
}
|
|
23893
|
-
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
}
|
|
23906
|
-
|
|
24393
|
+
function base32Decode(text) {
|
|
24394
|
+
const out = [];
|
|
24395
|
+
let buffer = 0;
|
|
24396
|
+
let bits = 0;
|
|
24397
|
+
for (const char of text) {
|
|
24398
|
+
const value = B32_ALPHABET.indexOf(char);
|
|
24399
|
+
if (value < 0) throw new Error("base32: character outside the alphabet");
|
|
24400
|
+
buffer = buffer << 5 | value;
|
|
24401
|
+
bits += 5;
|
|
24402
|
+
if (bits >= 8) {
|
|
24403
|
+
out.push(buffer >>> bits - 8 & 255);
|
|
24404
|
+
bits -= 8;
|
|
24405
|
+
}
|
|
24406
|
+
}
|
|
24407
|
+
return Buffer.from(out);
|
|
24408
|
+
}
|
|
24409
|
+
function encodeKeyVersion(version2) {
|
|
24410
|
+
if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
|
|
24411
|
+
throw new Error("vault: key version out of range");
|
|
24412
|
+
}
|
|
24413
|
+
const bytes = [];
|
|
24414
|
+
let remaining = version2;
|
|
24415
|
+
while (remaining > 0) {
|
|
24416
|
+
bytes.unshift(remaining & 255);
|
|
24417
|
+
remaining = Math.floor(remaining / 256);
|
|
24418
|
+
}
|
|
24419
|
+
return base32Encode(Uint8Array.from(bytes));
|
|
24420
|
+
}
|
|
24421
|
+
function decodeKeyVersion(encoded) {
|
|
24422
|
+
const bytes = base32Decode(encoded);
|
|
24423
|
+
if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
|
|
24424
|
+
let version2 = 0;
|
|
24425
|
+
for (const byte of bytes) version2 = version2 * 256 + byte;
|
|
24426
|
+
if (version2 < 1) throw new Error("vault: bad key version");
|
|
24427
|
+
return version2;
|
|
24428
|
+
}
|
|
24429
|
+
function deriveSubkeys(master) {
|
|
24430
|
+
const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
|
|
24431
|
+
return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
|
|
24432
|
+
}
|
|
24433
|
+
function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
|
|
24434
|
+
if (pointerId.length !== POINTER_ID_BYTES) {
|
|
24435
|
+
throw new Error("vault: pointer id must be 16 bytes");
|
|
24436
|
+
}
|
|
24437
|
+
const head = Buffer.alloc(6);
|
|
24438
|
+
head.writeUInt16BE(formatVersion, 0);
|
|
24439
|
+
head.writeUInt32BE(keyVersion, 2);
|
|
24440
|
+
return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
|
|
24441
|
+
}
|
|
24442
|
+
function seal(encKey, plaintext, aad, nonce) {
|
|
24443
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
|
|
24444
|
+
cipher.setAAD(aad);
|
|
24445
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
24446
|
+
return { ciphertext, nonce, authTag: cipher.getAuthTag() };
|
|
24447
|
+
}
|
|
24448
|
+
function open(encKey, sealed, aad) {
|
|
23907
24449
|
try {
|
|
23908
|
-
const
|
|
23909
|
-
|
|
24450
|
+
const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
|
|
24451
|
+
decipher.setAAD(aad);
|
|
24452
|
+
decipher.setAuthTag(sealed.authTag);
|
|
24453
|
+
return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
|
|
23910
24454
|
} catch {
|
|
23911
|
-
return
|
|
24455
|
+
return null;
|
|
23912
24456
|
}
|
|
23913
24457
|
}
|
|
23914
|
-
function
|
|
23915
|
-
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
if (
|
|
23919
|
-
const
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
24458
|
+
function signPointer(signKey, keyVersion, pointerId, category) {
|
|
24459
|
+
return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
|
|
24460
|
+
}
|
|
24461
|
+
function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
|
|
24462
|
+
if (tag.length !== TAG_BYTES) return false;
|
|
24463
|
+
const expected = signPointer(signKey, keyVersion, pointerId, category);
|
|
24464
|
+
return timingSafeEqual(expected, Buffer.from(tag));
|
|
24465
|
+
}
|
|
24466
|
+
function formatPointer(category, keyVersion, pointerId, tag) {
|
|
24467
|
+
return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
|
|
24468
|
+
}
|
|
24469
|
+
|
|
24470
|
+
// ../../packages/persistence/src/vault/key-provider.ts
|
|
24471
|
+
import { execFileSync } from "child_process";
|
|
24472
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
24473
|
+
import {
|
|
24474
|
+
chmodSync as chmodSync2,
|
|
24475
|
+
mkdirSync as mkdirSync2,
|
|
24476
|
+
readFileSync as readFileSync3,
|
|
24477
|
+
renameSync as renameSync4,
|
|
24478
|
+
rmSync as rmSync3,
|
|
24479
|
+
statSync,
|
|
24480
|
+
writeFileSync as writeFileSync2
|
|
24481
|
+
} from "fs";
|
|
24482
|
+
import { join as join5 } from "path";
|
|
24483
|
+
var VaultKeyEpochMissingError = class extends Error {
|
|
24484
|
+
version;
|
|
24485
|
+
constructor(version2) {
|
|
24486
|
+
super(`vault: key epoch ${String(version2)} is not present in the keyring`);
|
|
24487
|
+
this.name = "VaultKeyEpochMissingError";
|
|
24488
|
+
this.version = version2;
|
|
24489
|
+
}
|
|
24490
|
+
};
|
|
24491
|
+
var VAULT_KEY_FILENAME = "vault.key";
|
|
24492
|
+
var KEY_MATERIAL_BYTES2 = 32;
|
|
24493
|
+
var KEYCHAIN_SERVICE = "aka-vault";
|
|
24494
|
+
var KEYCHAIN_ACCOUNT = "keyring";
|
|
24495
|
+
function parseKeyring(raw) {
|
|
24496
|
+
const parsed = JSON.parse(raw);
|
|
24497
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
24498
|
+
throw new Error("vault key file is corrupt: not a JSON object");
|
|
24499
|
+
}
|
|
24500
|
+
const { current, keys } = parsed;
|
|
24501
|
+
if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
|
|
24502
|
+
throw new Error("vault key file is corrupt: bad current version");
|
|
24503
|
+
}
|
|
24504
|
+
if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
|
|
24505
|
+
throw new Error("vault key file is corrupt: bad keys map");
|
|
24506
|
+
}
|
|
24507
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
24508
|
+
for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
|
|
24509
|
+
const version2 = Number(rawVersion);
|
|
24510
|
+
if (!Number.isInteger(version2) || version2 < 1) {
|
|
24511
|
+
throw new Error("vault key file is corrupt: bad key version");
|
|
24512
|
+
}
|
|
24513
|
+
if (typeof rawMaterial !== "string") {
|
|
24514
|
+
throw new Error("vault key file is corrupt: bad key material");
|
|
23924
24515
|
}
|
|
24516
|
+
const bytes = Buffer.from(rawMaterial, "base64");
|
|
24517
|
+
if (bytes.length !== KEY_MATERIAL_BYTES2) {
|
|
24518
|
+
throw new Error("vault key file is corrupt: bad key material length");
|
|
24519
|
+
}
|
|
24520
|
+
map2.set(version2, bytes);
|
|
23925
24521
|
}
|
|
23926
|
-
|
|
24522
|
+
if (!map2.has(current)) {
|
|
24523
|
+
throw new Error("vault key file is corrupt: current version has no material");
|
|
24524
|
+
}
|
|
24525
|
+
return { current, keys: map2 };
|
|
23927
24526
|
}
|
|
23928
|
-
function
|
|
23929
|
-
const
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
if (id.startsWith("claude-")) return "anthropic";
|
|
23936
|
-
return "unknown";
|
|
24527
|
+
function serializeKeyring(keyring) {
|
|
24528
|
+
const keys = {};
|
|
24529
|
+
for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
|
|
24530
|
+
const material = keyring.keys.get(version2);
|
|
24531
|
+
if (material) keys[String(version2)] = material.toString("base64");
|
|
24532
|
+
}
|
|
24533
|
+
return JSON.stringify({ current: keyring.current, keys });
|
|
23937
24534
|
}
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
24535
|
+
function mintKeyring() {
|
|
24536
|
+
return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
|
|
24537
|
+
}
|
|
24538
|
+
function withNextEpoch(keyring) {
|
|
24539
|
+
const next = Math.max(...keyring.keys.keys()) + 1;
|
|
24540
|
+
const keys = new Map(keyring.keys);
|
|
24541
|
+
keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
|
|
24542
|
+
return { current: next, keys };
|
|
24543
|
+
}
|
|
24544
|
+
function currentOf(keyring) {
|
|
24545
|
+
const material = keyring.keys.get(keyring.current);
|
|
24546
|
+
if (!material) throw new VaultKeyEpochMissingError(keyring.current);
|
|
24547
|
+
return { material, version: keyring.current };
|
|
24548
|
+
}
|
|
24549
|
+
function epochOf(keyring, version2) {
|
|
24550
|
+
const material = keyring.keys.get(version2);
|
|
24551
|
+
if (!material) throw new VaultKeyEpochMissingError(version2);
|
|
24552
|
+
return { material, version: version2 };
|
|
24553
|
+
}
|
|
24554
|
+
function asAsync(work) {
|
|
23941
24555
|
try {
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
} catch {
|
|
24556
|
+
return Promise.resolve(work());
|
|
24557
|
+
} catch (err) {
|
|
24558
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
23946
24559
|
}
|
|
23947
|
-
|
|
23948
|
-
|
|
24560
|
+
}
|
|
24561
|
+
function asError(err) {
|
|
24562
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
24563
|
+
}
|
|
24564
|
+
var ROTATION_LOCK_STALE_MS = 6e4;
|
|
24565
|
+
var LOCK_OWNER_FILE = "owner";
|
|
24566
|
+
var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
|
|
24567
|
+
function claimRotationLock(lock, owner) {
|
|
24568
|
+
try {
|
|
24569
|
+
mkdirSync2(lock);
|
|
24570
|
+
} catch (err) {
|
|
24571
|
+
if (err.code === "EEXIST") return false;
|
|
24572
|
+
throw asError(err);
|
|
24573
|
+
}
|
|
24574
|
+
try {
|
|
24575
|
+
writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
|
|
24576
|
+
`, { mode: DATA_FILE_MODE });
|
|
24577
|
+
return true;
|
|
24578
|
+
} catch (err) {
|
|
24579
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
24580
|
+
throw asError(err);
|
|
24581
|
+
}
|
|
24582
|
+
}
|
|
24583
|
+
function acquireRotationLock(keysDir2) {
|
|
24584
|
+
const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
24585
|
+
const owner = randomBytes2(16).toString("hex");
|
|
24586
|
+
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
24587
|
+
let held;
|
|
24588
|
+
try {
|
|
24589
|
+
held = statSync(lock);
|
|
24590
|
+
} catch {
|
|
24591
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
24592
|
+
}
|
|
24593
|
+
if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
|
|
24594
|
+
const aside = `${lock}.stale.${owner}`;
|
|
24595
|
+
try {
|
|
24596
|
+
const now = statSync(lock);
|
|
24597
|
+
if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
|
|
24598
|
+
throw new Error(ROTATION_IN_PROGRESS);
|
|
24599
|
+
}
|
|
24600
|
+
renameSync4(lock, aside);
|
|
24601
|
+
} catch (err) {
|
|
24602
|
+
if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
|
|
24603
|
+
throw new Error(ROTATION_IN_PROGRESS, { cause: err });
|
|
24604
|
+
}
|
|
24605
|
+
rmSync3(aside, { recursive: true, force: true });
|
|
24606
|
+
if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
|
|
24607
|
+
return { lock, owner };
|
|
24608
|
+
}
|
|
24609
|
+
function releaseRotationLock(lease) {
|
|
24610
|
+
try {
|
|
24611
|
+
if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
24612
|
+
} catch {
|
|
24613
|
+
return;
|
|
24614
|
+
}
|
|
24615
|
+
rmSync3(lease.lock, { recursive: true, force: true });
|
|
24616
|
+
}
|
|
24617
|
+
function withRotationLock(keysDir2, work) {
|
|
24618
|
+
ensureDataDirSync(keysDir2);
|
|
24619
|
+
const lease = acquireRotationLock(keysDir2);
|
|
24620
|
+
try {
|
|
24621
|
+
return work();
|
|
24622
|
+
} finally {
|
|
24623
|
+
releaseRotationLock(lease);
|
|
24624
|
+
}
|
|
24625
|
+
}
|
|
24626
|
+
var FileKeyProvider = class {
|
|
24627
|
+
#keysDir;
|
|
24628
|
+
constructor(keysDir2) {
|
|
24629
|
+
this.#keysDir = keysDir2;
|
|
24630
|
+
}
|
|
24631
|
+
get filePath() {
|
|
24632
|
+
return join5(this.#keysDir, VAULT_KEY_FILENAME);
|
|
24633
|
+
}
|
|
24634
|
+
loadOrCreate() {
|
|
24635
|
+
return asAsync(() => {
|
|
24636
|
+
const existing = this.#read();
|
|
24637
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
24638
|
+
tightenFileMode(this.filePath);
|
|
24639
|
+
return currentOf(existing);
|
|
24640
|
+
});
|
|
24641
|
+
}
|
|
24642
|
+
rotate() {
|
|
24643
|
+
return asAsync(
|
|
24644
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
24645
|
+
const existing = this.#read();
|
|
24646
|
+
if (!existing) return currentOf(this.#createExclusive());
|
|
24647
|
+
return currentOf(this.#write(withNextEpoch(existing)));
|
|
24648
|
+
})
|
|
24649
|
+
);
|
|
24650
|
+
}
|
|
24651
|
+
materialFor(version2) {
|
|
24652
|
+
return asAsync(() => {
|
|
24653
|
+
const existing = this.#read();
|
|
24654
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
24655
|
+
return epochOf(existing, version2);
|
|
24656
|
+
});
|
|
24657
|
+
}
|
|
24658
|
+
/** The keyring, or null when the file is ABSENT. A corrupt file throws. */
|
|
24659
|
+
#read() {
|
|
24660
|
+
let raw;
|
|
24661
|
+
try {
|
|
24662
|
+
raw = readFileSync3(this.filePath, "utf8");
|
|
24663
|
+
} catch (err) {
|
|
24664
|
+
if (err.code === "ENOENT") return null;
|
|
24665
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
24666
|
+
}
|
|
24667
|
+
return parseKeyring(raw);
|
|
24668
|
+
}
|
|
24669
|
+
/**
|
|
24670
|
+
* First mint: the keyring is created at its FINAL path with a
|
|
24671
|
+
* creation-exclusive write, so two processes racing a fresh machine cannot
|
|
24672
|
+
* each mint a different epoch 1 — with tmp + rename the loser's replace
|
|
24673
|
+
* would orphan everything the winner had already sealed. On EEXIST the
|
|
24674
|
+
* loser re-reads and adopts the winner's keyring; it minted nothing.
|
|
24675
|
+
* Atomic replace is unnecessary here: nothing can be mid-read of a file
|
|
24676
|
+
* that did not exist, and a torn exclusive write parses as corrupt on the
|
|
24677
|
+
* next read and fails secure rather than being re-minted over.
|
|
24678
|
+
*/
|
|
24679
|
+
#createExclusive() {
|
|
24680
|
+
ensureDataDirSync(this.#keysDir);
|
|
24681
|
+
const keyring = mintKeyring();
|
|
24682
|
+
try {
|
|
24683
|
+
writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
|
|
24684
|
+
`, {
|
|
24685
|
+
flag: "wx",
|
|
24686
|
+
mode: DATA_FILE_MODE
|
|
24687
|
+
});
|
|
24688
|
+
} catch (err) {
|
|
24689
|
+
if (err.code !== "EEXIST") throw asError(err);
|
|
24690
|
+
const winner = this.#read();
|
|
24691
|
+
if (!winner) {
|
|
24692
|
+
throw new Error("vault: key file vanished during first mint", { cause: err });
|
|
24693
|
+
}
|
|
24694
|
+
return winner;
|
|
24695
|
+
}
|
|
24696
|
+
tightenFileMode(this.filePath);
|
|
24697
|
+
return keyring;
|
|
24698
|
+
}
|
|
24699
|
+
/**
|
|
24700
|
+
* Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
|
|
24701
|
+
* Used only for rotation, under the rotation lock — first creation goes
|
|
24702
|
+
* through the creation-exclusive path instead.
|
|
24703
|
+
*/
|
|
24704
|
+
#write(keyring) {
|
|
24705
|
+
ensureDataDirSync(this.#keysDir);
|
|
24706
|
+
const file2 = this.filePath;
|
|
24707
|
+
const tmp = `${file2}.tmp`;
|
|
24708
|
+
writeFileSync2(tmp, `${serializeKeyring(keyring)}
|
|
24709
|
+
`, { mode: DATA_FILE_MODE });
|
|
24710
|
+
renameSync4(tmp, file2);
|
|
24711
|
+
tightenFileMode(file2);
|
|
24712
|
+
return keyring;
|
|
24713
|
+
}
|
|
24714
|
+
};
|
|
24715
|
+
function tightenFileMode(file2) {
|
|
24716
|
+
try {
|
|
24717
|
+
chmodSync2(file2, DATA_FILE_MODE);
|
|
24718
|
+
} catch {
|
|
24719
|
+
}
|
|
24720
|
+
}
|
|
24721
|
+
var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
|
|
24722
|
+
encoding: "utf8",
|
|
24723
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
24724
|
+
});
|
|
24725
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
24726
|
+
var KeychainKeyProvider = class {
|
|
24727
|
+
#keysDir;
|
|
24728
|
+
#exec;
|
|
24729
|
+
constructor(keysDir2, exec = runSecurity) {
|
|
24730
|
+
if (exec === runSecurity && process.platform !== "darwin") {
|
|
24731
|
+
throw new Error(
|
|
24732
|
+
`keychain custody is not available on this platform (${process.platform}); use file custody`
|
|
24733
|
+
);
|
|
24734
|
+
}
|
|
24735
|
+
this.#keysDir = keysDir2;
|
|
24736
|
+
this.#exec = exec;
|
|
24737
|
+
}
|
|
24738
|
+
/** Where a fallback file provider for the same vault would keep its keyring. */
|
|
24739
|
+
get keysDir() {
|
|
24740
|
+
return this.#keysDir;
|
|
24741
|
+
}
|
|
24742
|
+
loadOrCreate() {
|
|
24743
|
+
return asAsync(() => {
|
|
24744
|
+
const existing = this.#read();
|
|
24745
|
+
if (existing) return currentOf(existing);
|
|
24746
|
+
return currentOf(this.#create(mintKeyring()));
|
|
24747
|
+
});
|
|
24748
|
+
}
|
|
24749
|
+
rotate() {
|
|
24750
|
+
return asAsync(
|
|
24751
|
+
() => withRotationLock(this.#keysDir, () => {
|
|
24752
|
+
const existing = this.#read();
|
|
24753
|
+
if (!existing) return currentOf(this.#create(mintKeyring()));
|
|
24754
|
+
return currentOf(this.#replace(withNextEpoch(existing)));
|
|
24755
|
+
})
|
|
24756
|
+
);
|
|
24757
|
+
}
|
|
24758
|
+
materialFor(version2) {
|
|
24759
|
+
return asAsync(() => {
|
|
24760
|
+
const existing = this.#read();
|
|
24761
|
+
if (!existing) throw new VaultKeyEpochMissingError(version2);
|
|
24762
|
+
return epochOf(existing, version2);
|
|
24763
|
+
});
|
|
24764
|
+
}
|
|
24765
|
+
/** The keyring, or null when no item exists yet. A corrupt item throws. */
|
|
24766
|
+
#read() {
|
|
24767
|
+
let raw;
|
|
24768
|
+
try {
|
|
24769
|
+
raw = this.#exec([
|
|
24770
|
+
"find-generic-password",
|
|
24771
|
+
"-s",
|
|
24772
|
+
KEYCHAIN_SERVICE,
|
|
24773
|
+
"-a",
|
|
24774
|
+
KEYCHAIN_ACCOUNT,
|
|
24775
|
+
"-w"
|
|
24776
|
+
]);
|
|
24777
|
+
} catch (err) {
|
|
24778
|
+
if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
|
|
24779
|
+
throw new Error(
|
|
24780
|
+
`vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
|
|
24781
|
+
{ cause: err }
|
|
24782
|
+
);
|
|
24783
|
+
}
|
|
24784
|
+
const body = raw.trim();
|
|
24785
|
+
if (body.length === 0) return null;
|
|
24786
|
+
return parseKeyring(body);
|
|
24787
|
+
}
|
|
24788
|
+
/**
|
|
24789
|
+
* First mint: a plain `add-generic-password` (no `-U`) fails when an item
|
|
24790
|
+
* already exists, so a concurrent first mint cannot overwrite the winner's
|
|
24791
|
+
* keyring — the loser re-reads and adopts it instead.
|
|
24792
|
+
*/
|
|
24793
|
+
#create(keyring) {
|
|
24794
|
+
const args = [
|
|
24795
|
+
"add-generic-password",
|
|
24796
|
+
"-s",
|
|
24797
|
+
KEYCHAIN_SERVICE,
|
|
24798
|
+
"-a",
|
|
24799
|
+
KEYCHAIN_ACCOUNT,
|
|
24800
|
+
"-w",
|
|
24801
|
+
serializeKeyring(keyring)
|
|
24802
|
+
];
|
|
24803
|
+
try {
|
|
24804
|
+
this.#exec(args);
|
|
24805
|
+
} catch (err) {
|
|
24806
|
+
const winner = this.#read();
|
|
24807
|
+
if (winner) return winner;
|
|
24808
|
+
throw asError(err);
|
|
24809
|
+
}
|
|
24810
|
+
return keyring;
|
|
24811
|
+
}
|
|
24812
|
+
// `-U` updates the item in place, deliberately replacing the stored map with
|
|
24813
|
+
// one that contains it — used only for rotation, under the rotation lock.
|
|
24814
|
+
#replace(keyring) {
|
|
24815
|
+
this.#exec([
|
|
24816
|
+
"add-generic-password",
|
|
24817
|
+
"-U",
|
|
24818
|
+
"-s",
|
|
24819
|
+
KEYCHAIN_SERVICE,
|
|
24820
|
+
"-a",
|
|
24821
|
+
KEYCHAIN_ACCOUNT,
|
|
24822
|
+
"-w",
|
|
24823
|
+
serializeKeyring(keyring)
|
|
24824
|
+
]);
|
|
24825
|
+
return keyring;
|
|
24826
|
+
}
|
|
24827
|
+
};
|
|
24828
|
+
function createKeyProvider(custody, keysDir2) {
|
|
24829
|
+
if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
|
|
24830
|
+
return new FileKeyProvider(keysDir2);
|
|
24831
|
+
}
|
|
24832
|
+
|
|
24833
|
+
// ../../packages/persistence/src/vault/vault.ts
|
|
24834
|
+
import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
|
|
24835
|
+
var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
|
|
24836
|
+
var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
|
|
24837
|
+
var VAULT_PURGE_POINTER_ID = "*";
|
|
24838
|
+
function parsePointer(token) {
|
|
24839
|
+
if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
|
|
24840
|
+
const body = token.slice("[[aka:".length, -"]]".length);
|
|
24841
|
+
const colon = body.indexOf(":");
|
|
24842
|
+
if (colon < 0) return null;
|
|
24843
|
+
const category = body.slice(0, colon);
|
|
24844
|
+
const [kv, id, tag] = body.slice(colon + 1).split(".");
|
|
24845
|
+
if (kv === void 0 || id === void 0 || tag === void 0) return null;
|
|
24846
|
+
try {
|
|
24847
|
+
const keyVersion = decodeKeyVersion(kv);
|
|
24848
|
+
const pointerId = base32Decode(id);
|
|
24849
|
+
const tagBytes = base32Decode(tag);
|
|
24850
|
+
if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
|
|
24851
|
+
return null;
|
|
24852
|
+
}
|
|
24853
|
+
return { category, keyVersion, pointerId, tag: tagBytes };
|
|
24854
|
+
} catch {
|
|
24855
|
+
return null;
|
|
24856
|
+
}
|
|
24857
|
+
}
|
|
24858
|
+
var SecretVault = class {
|
|
24859
|
+
#repo;
|
|
24860
|
+
#keys;
|
|
24861
|
+
#fingerprintKey;
|
|
24862
|
+
#isConsented;
|
|
24863
|
+
#verifyGrant;
|
|
24864
|
+
#now;
|
|
24865
|
+
constructor(deps) {
|
|
24866
|
+
this.#repo = deps.repo;
|
|
24867
|
+
this.#keys = deps.keys;
|
|
24868
|
+
this.#fingerprintKey = deps.fingerprintKey;
|
|
24869
|
+
this.#isConsented = deps.isConsented;
|
|
24870
|
+
this.#verifyGrant = deps.verifyGrant;
|
|
24871
|
+
this.#now = deps.now ?? (() => Date.now());
|
|
24872
|
+
}
|
|
24873
|
+
/**
|
|
24874
|
+
* Store a value and return the pointer that stands for it. The same value
|
|
24875
|
+
* always yields the same pointer on this machine — one row, one pointer id,
|
|
24876
|
+
* one category — which is what makes dedup and reuse counting work.
|
|
24877
|
+
*/
|
|
24878
|
+
async tokenize(raw, meta3) {
|
|
24879
|
+
if (!this.#isConsented()) return CONSENT_ABSENT;
|
|
24880
|
+
const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
|
|
24881
|
+
const existing = this.#repo.byValueFingerprint(valueFingerprint);
|
|
24882
|
+
const now = this.#now();
|
|
24883
|
+
if (existing) {
|
|
24884
|
+
this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
|
|
24885
|
+
return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
|
|
24886
|
+
}
|
|
24887
|
+
const { material, version: version2 } = await this.#keys.loadOrCreate();
|
|
24888
|
+
const subkeys = deriveSubkeys(material);
|
|
24889
|
+
const pointerId = randomBytes3(POINTER_ID_BYTES);
|
|
24890
|
+
const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
|
|
24891
|
+
const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
|
|
24892
|
+
const { row } = this.#repo.upsert(
|
|
24893
|
+
{
|
|
24894
|
+
pointerId: base32Encode(pointerId),
|
|
24895
|
+
valueFingerprint,
|
|
24896
|
+
fingerprintKeyVersion: this.#fingerprintKey.version,
|
|
24897
|
+
keyVersion: version2,
|
|
24898
|
+
// Recorded so the row stays OPENABLE if the wire-format constant ever
|
|
24899
|
+
// moves: it is part of this row's AEAD AAD. It is not a tag input —
|
|
24900
|
+
// tags are pinned to the constant on both sides.
|
|
24901
|
+
formatVersion: POINTER_FORMAT_VERSION,
|
|
24902
|
+
category: meta3.category,
|
|
24903
|
+
ruleId: meta3.ruleId,
|
|
24904
|
+
maskedMatch: meta3.maskedMatch,
|
|
24905
|
+
provider: meta3.provider,
|
|
24906
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
24907
|
+
nonce: sealed.nonce.toString("base64"),
|
|
24908
|
+
authTag: sealed.authTag.toString("base64")
|
|
24909
|
+
},
|
|
24910
|
+
now
|
|
24911
|
+
);
|
|
24912
|
+
return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
|
|
24913
|
+
}
|
|
24914
|
+
/**
|
|
24915
|
+
* Resolve a pointer back to its value, for a human or (with a grant) for the
|
|
24916
|
+
* model. Every call that gets as far as an identified row writes an audit row.
|
|
24917
|
+
*/
|
|
24918
|
+
async detokenize(token, opts) {
|
|
24919
|
+
const parsed = parsePointer(token);
|
|
24920
|
+
if (!parsed) return UNAVAILABLE;
|
|
24921
|
+
let signKey;
|
|
24922
|
+
try {
|
|
24923
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
24924
|
+
signKey = deriveSubkeys(epoch.material).sign;
|
|
24925
|
+
} catch {
|
|
24926
|
+
return UNAVAILABLE;
|
|
24927
|
+
}
|
|
24928
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
24929
|
+
return UNAVAILABLE;
|
|
24930
|
+
}
|
|
24931
|
+
const pointerId = base32Encode(parsed.pointerId);
|
|
24932
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
24933
|
+
if (!row) {
|
|
24934
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
24935
|
+
return UNAVAILABLE;
|
|
24936
|
+
}
|
|
24937
|
+
if (row.category !== parsed.category) return UNAVAILABLE;
|
|
24938
|
+
if (opts.target === "model") {
|
|
24939
|
+
const grantId = opts.grantId;
|
|
24940
|
+
const verify = this.#verifyGrant;
|
|
24941
|
+
if (verify === void 0 || grantId === void 0 || grantId === "") {
|
|
24942
|
+
this.#audit(pointerId, opts, "refused");
|
|
24943
|
+
return UNAVAILABLE;
|
|
24944
|
+
}
|
|
24945
|
+
let covered;
|
|
24946
|
+
try {
|
|
24947
|
+
covered = await verify(grantId, {
|
|
24948
|
+
ruleId: row.ruleId,
|
|
24949
|
+
valueFingerprint: row.valueFingerprint,
|
|
24950
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
24951
|
+
});
|
|
24952
|
+
} catch {
|
|
24953
|
+
covered = false;
|
|
24954
|
+
}
|
|
24955
|
+
if (!covered) {
|
|
24956
|
+
this.#audit(pointerId, opts, "refused");
|
|
24957
|
+
return UNAVAILABLE;
|
|
24958
|
+
}
|
|
24959
|
+
}
|
|
24960
|
+
let raw;
|
|
24961
|
+
try {
|
|
24962
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
24963
|
+
raw = open(
|
|
24964
|
+
deriveSubkeys(epoch.material).enc,
|
|
24965
|
+
{
|
|
24966
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
24967
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
24968
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
24969
|
+
},
|
|
24970
|
+
// Sealed under the ROW's epoch and format version. Rotation may have
|
|
24971
|
+
// moved the epoch past the one this token names, and a format bump may
|
|
24972
|
+
// have moved the constant past the generation this row was sealed
|
|
24973
|
+
// under — the AAD follows the row in both cases, never the token.
|
|
24974
|
+
bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
|
|
24975
|
+
);
|
|
24976
|
+
} catch {
|
|
24977
|
+
raw = null;
|
|
24978
|
+
}
|
|
24979
|
+
if (raw === null) {
|
|
24980
|
+
this.#audit(pointerId, opts, "unavailable");
|
|
24981
|
+
return UNAVAILABLE;
|
|
24982
|
+
}
|
|
24983
|
+
this.#audit(pointerId, opts, "revealed");
|
|
24984
|
+
return raw;
|
|
24985
|
+
}
|
|
24986
|
+
/**
|
|
24987
|
+
* Owner-surface reveal by row id: the dashboard shows a row the owner can
|
|
24988
|
+
* already see and asks for its value. There is no wire token here to verify —
|
|
24989
|
+
* the tag exists to stop FORGED tokens arriving in untrusted text, and a row
|
|
24990
|
+
* id selected server-side from the owner's own store is not that — so this
|
|
24991
|
+
* loads the row directly, opens its ciphertext under the row's epoch, and
|
|
24992
|
+
* audits exactly like a human-target de-reference. Never callable with
|
|
24993
|
+
* target 'model': the wire-token path with its grant gate is the only road
|
|
24994
|
+
* raw travels toward the model.
|
|
24995
|
+
*/
|
|
24996
|
+
async revealEntry(pointerId, opts) {
|
|
24997
|
+
const row = this.#repo.byPointerId(pointerId);
|
|
24998
|
+
if (!row) {
|
|
24999
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
25000
|
+
return UNAVAILABLE;
|
|
25001
|
+
}
|
|
25002
|
+
const raw = await this.#openRow(row);
|
|
25003
|
+
if (raw === null) {
|
|
25004
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
|
|
25005
|
+
return UNAVAILABLE;
|
|
25006
|
+
}
|
|
25007
|
+
this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
|
|
25008
|
+
return raw;
|
|
25009
|
+
}
|
|
25010
|
+
/** Badge and listing data. No raw value, no fingerprint, and no audit row. */
|
|
25011
|
+
async describePointer(token) {
|
|
25012
|
+
const row = await this.#rowFor(token);
|
|
25013
|
+
if (!row) return null;
|
|
25014
|
+
return {
|
|
25015
|
+
category: row.category,
|
|
25016
|
+
...row.provider === void 0 ? {} : { provider: row.provider },
|
|
25017
|
+
maskedMatch: row.maskedMatch,
|
|
25018
|
+
occurrences: row.occurrenceCount,
|
|
25019
|
+
firstSeen: new Date(row.firstSeen).toISOString(),
|
|
25020
|
+
lastSeen: new Date(row.lastSeen).toISOString()
|
|
25021
|
+
};
|
|
25022
|
+
}
|
|
25023
|
+
/**
|
|
25024
|
+
* The raw-free row identity a reveal grant matches on. Deliberately not fed to
|
|
25025
|
+
* view surfaces: the keyed fingerprint is a correlation key and must not reach
|
|
25026
|
+
* a presentation layer.
|
|
25027
|
+
*/
|
|
25028
|
+
async resolvePointerIdentity(token) {
|
|
25029
|
+
const row = await this.#rowFor(token);
|
|
25030
|
+
if (!row) return null;
|
|
25031
|
+
return {
|
|
25032
|
+
ruleId: row.ruleId,
|
|
25033
|
+
valueFingerprint: row.valueFingerprint,
|
|
25034
|
+
fingerprintKeyVersion: row.fingerprintKeyVersion
|
|
25035
|
+
};
|
|
25036
|
+
}
|
|
25037
|
+
/**
|
|
25038
|
+
* Mint the next vault key epoch and re-encrypt every entry under it. Pointers
|
|
25039
|
+
* already emitted keep verifying: their tag is checked against the historical
|
|
25040
|
+
* epoch they name, which the key provider retains.
|
|
25041
|
+
*
|
|
25042
|
+
* Safe to interrupt — each row carries the epoch its ciphertext is sealed
|
|
25043
|
+
* under, so a half-finished pass leaves every row openable.
|
|
25044
|
+
*
|
|
25045
|
+
* The rotation lock covers only the keyring mint inside `rotate()`; the
|
|
25046
|
+
* re-seal pass below runs unlocked. Two concurrent rotations therefore
|
|
25047
|
+
* serialize on the keyring but interleave over the rows, so a slower pass can
|
|
25048
|
+
* re-seal a row back to an epoch a faster one already moved past, and
|
|
25049
|
+
* `reEncrypted` can double-count. No value is lost either way — every epoch is
|
|
25050
|
+
* retained and every row stays openable — but "after rotation every row sits
|
|
25051
|
+
* at the newest epoch" does not hold under concurrency. Holding the lock
|
|
25052
|
+
* across the whole pass requires an async-aware lock, since a callback that
|
|
25053
|
+
* awaits would release the lock at its first suspension.
|
|
25054
|
+
*/
|
|
25055
|
+
async rotateVaultKey() {
|
|
25056
|
+
const next = await this.#keys.rotate();
|
|
25057
|
+
const nextEnc = deriveSubkeys(next.material).enc;
|
|
25058
|
+
let reEncrypted = 0;
|
|
25059
|
+
for (const row of this.#repo.listAll()) {
|
|
25060
|
+
if (row.keyVersion === next.version) continue;
|
|
25061
|
+
const pointerId = base32Decode(row.pointerId);
|
|
25062
|
+
let raw;
|
|
25063
|
+
try {
|
|
25064
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
25065
|
+
raw = open(
|
|
25066
|
+
deriveSubkeys(epoch.material).enc,
|
|
25067
|
+
{
|
|
25068
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
25069
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
25070
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
25071
|
+
},
|
|
25072
|
+
bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
|
|
25073
|
+
);
|
|
25074
|
+
} catch {
|
|
25075
|
+
raw = null;
|
|
25076
|
+
}
|
|
25077
|
+
if (raw === null) continue;
|
|
25078
|
+
const sealed = seal(
|
|
25079
|
+
nextEnc,
|
|
25080
|
+
raw,
|
|
25081
|
+
bindingInput(next.version, pointerId, row.category, row.formatVersion),
|
|
25082
|
+
randomBytes3(NONCE_BYTES)
|
|
25083
|
+
);
|
|
25084
|
+
this.#repo.replaceCiphertext(row.pointerId, {
|
|
25085
|
+
keyVersion: next.version,
|
|
25086
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
25087
|
+
nonce: sealed.nonce.toString("base64"),
|
|
25088
|
+
authTag: sealed.authTag.toString("base64")
|
|
25089
|
+
});
|
|
25090
|
+
reEncrypted += 1;
|
|
25091
|
+
}
|
|
25092
|
+
return { version: next.version, reEncrypted };
|
|
25093
|
+
}
|
|
25094
|
+
/**
|
|
25095
|
+
* Re-key every entry's value fingerprint after the exception key rotates,
|
|
25096
|
+
* PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
|
|
25097
|
+
* because the raw values are gone — the vault still holds the values, so
|
|
25098
|
+
* determinism, dedup, and every outstanding pointer survive the rotation.
|
|
25099
|
+
*
|
|
25100
|
+
* Every fingerprint-key rotation must run this: a row left at the old epoch
|
|
25101
|
+
* still resolves, but the same value detected again fingerprints under the
|
|
25102
|
+
* NEW key, misses the dedup lookup, and mints a second row and a second
|
|
25103
|
+
* pointer — one value, two tokens in circulation.
|
|
25104
|
+
*
|
|
25105
|
+
* Per-row best-effort: a row that cannot open, or whose refreshed
|
|
25106
|
+
* fingerprint collides with a row already refreshed, is skipped rather than
|
|
25107
|
+
* aborting the pass — one damaged entry must not strand the re-key of every
|
|
25108
|
+
* other. A skipped row keeps resolving under its old fingerprint epoch.
|
|
25109
|
+
*/
|
|
25110
|
+
async refreshFingerprints(next) {
|
|
25111
|
+
let refreshed = 0;
|
|
25112
|
+
for (const row of this.#repo.listAll()) {
|
|
25113
|
+
try {
|
|
25114
|
+
const raw = await this.#openRow(row);
|
|
25115
|
+
if (raw === null) continue;
|
|
25116
|
+
this.#repo.refreshFingerprint(row.pointerId, {
|
|
25117
|
+
valueFingerprint: fingerprintValue(next, raw),
|
|
25118
|
+
fingerprintKeyVersion: next.version
|
|
25119
|
+
});
|
|
25120
|
+
refreshed += 1;
|
|
25121
|
+
} catch {
|
|
25122
|
+
continue;
|
|
25123
|
+
}
|
|
25124
|
+
}
|
|
25125
|
+
return refreshed;
|
|
25126
|
+
}
|
|
25127
|
+
/**
|
|
25128
|
+
* Destroy every entry, making all outstanding pointers permanently
|
|
25129
|
+
* unresolvable.
|
|
25130
|
+
*
|
|
25131
|
+
* The count comes from `purgeAll` rather than a separate `countEntries` —
|
|
25132
|
+
* `purgeAll` counts inside the same transaction that deletes, so the audit row
|
|
25133
|
+
* reports what was actually destroyed. Counting beforehand would let a
|
|
25134
|
+
* concurrent write land between the two statements and put a number in the
|
|
25135
|
+
* durable record that never matched reality.
|
|
25136
|
+
*/
|
|
25137
|
+
purgeVault() {
|
|
25138
|
+
const destroyed = this.#repo.purgeAll();
|
|
25139
|
+
this.#repo.recordDeref({
|
|
25140
|
+
id: randomUUID10(),
|
|
25141
|
+
pointerId: VAULT_PURGE_POINTER_ID,
|
|
25142
|
+
at: this.#now(),
|
|
25143
|
+
target: "human",
|
|
25144
|
+
reason: "purge",
|
|
25145
|
+
outcome: "unavailable",
|
|
25146
|
+
pointerCount: Math.max(destroyed, 1)
|
|
25147
|
+
});
|
|
25148
|
+
return destroyed;
|
|
25149
|
+
}
|
|
25150
|
+
// Sign under the epoch the token names — which for a re-detected value is the
|
|
25151
|
+
// epoch its row currently sits at rather than whatever is current.
|
|
25152
|
+
//
|
|
25153
|
+
// The row's format version is NOT a tag input. It binds the row's ciphertext
|
|
25154
|
+
// (it is part of the AEAD AAD, so an old row stays openable) but never the
|
|
25155
|
+
// wire tag, which verification checks against POINTER_FORMAT_VERSION without
|
|
25156
|
+
// knowing any row. Signing a token here under a row's own generation is what
|
|
25157
|
+
// would make the vault emit tokens it then refuses.
|
|
25158
|
+
async #emitToken(keyVersion, pointerIdB32, category) {
|
|
25159
|
+
const pointerId = base32Decode(pointerIdB32);
|
|
25160
|
+
const epoch = await this.#keys.materialFor(keyVersion);
|
|
25161
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
25162
|
+
return formatPointer(
|
|
25163
|
+
category,
|
|
25164
|
+
keyVersion,
|
|
25165
|
+
pointerId,
|
|
25166
|
+
signPointer(signKey, keyVersion, pointerId, category)
|
|
25167
|
+
);
|
|
25168
|
+
}
|
|
25169
|
+
async #openRow(row) {
|
|
25170
|
+
try {
|
|
25171
|
+
const epoch = await this.#keys.materialFor(row.keyVersion);
|
|
25172
|
+
return open(
|
|
25173
|
+
deriveSubkeys(epoch.material).enc,
|
|
25174
|
+
{
|
|
25175
|
+
ciphertext: Buffer.from(row.ciphertext, "base64"),
|
|
25176
|
+
nonce: Buffer.from(row.nonce, "base64"),
|
|
25177
|
+
authTag: Buffer.from(row.authTag, "base64")
|
|
25178
|
+
},
|
|
25179
|
+
bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
|
|
25180
|
+
);
|
|
25181
|
+
} catch {
|
|
25182
|
+
return null;
|
|
25183
|
+
}
|
|
25184
|
+
}
|
|
25185
|
+
// Shared lookup for the read-only surfaces. It verifies the tag exactly as
|
|
25186
|
+
// detokenize does: a descriptor is not raw, but a token nobody can vouch for
|
|
25187
|
+
// should not resolve to anything at all — otherwise a fabricated pointer, or a
|
|
25188
|
+
// lookalike planted in a file, would still yield a category and a masked
|
|
25189
|
+
// preview. Verifying needs the historical epoch's key, which is why these
|
|
25190
|
+
// surfaces are async.
|
|
25191
|
+
async #rowFor(token) {
|
|
25192
|
+
const parsed = parsePointer(token);
|
|
25193
|
+
if (!parsed) return null;
|
|
25194
|
+
try {
|
|
25195
|
+
const epoch = await this.#keys.materialFor(parsed.keyVersion);
|
|
25196
|
+
const signKey = deriveSubkeys(epoch.material).sign;
|
|
25197
|
+
if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
|
|
25198
|
+
return null;
|
|
25199
|
+
}
|
|
25200
|
+
} catch {
|
|
25201
|
+
return null;
|
|
25202
|
+
}
|
|
25203
|
+
const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
|
|
25204
|
+
if (row?.category !== parsed.category) return null;
|
|
25205
|
+
return row;
|
|
25206
|
+
}
|
|
25207
|
+
#audit(pointerId, opts, outcome) {
|
|
25208
|
+
this.#repo.recordDeref({
|
|
25209
|
+
id: randomUUID10(),
|
|
25210
|
+
pointerId,
|
|
25211
|
+
at: this.#now(),
|
|
25212
|
+
target: opts.target,
|
|
25213
|
+
reason: opts.reason,
|
|
25214
|
+
outcome,
|
|
25215
|
+
...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
|
|
25216
|
+
// Only the batched reasons carry a count above one; a model crossing is
|
|
25217
|
+
// always its own row.
|
|
25218
|
+
pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
|
|
25219
|
+
});
|
|
25220
|
+
}
|
|
25221
|
+
};
|
|
25222
|
+
|
|
25223
|
+
// ../../packages/persistence/src/warn-era-cap.ts
|
|
25224
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
25225
|
+
import { join as join6 } from "path";
|
|
25226
|
+
var MARKER = "warn-era-capped";
|
|
25227
|
+
function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
|
|
25228
|
+
if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
|
|
25229
|
+
const marker = join6(dataDir2, MARKER);
|
|
25230
|
+
if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
|
|
25231
|
+
const capped = db.policies.capCategoryActions();
|
|
25232
|
+
writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
|
|
25233
|
+
`, { mode: DATA_FILE_MODE });
|
|
25234
|
+
return { capped };
|
|
25235
|
+
}
|
|
25236
|
+
|
|
25237
|
+
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
25238
|
+
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
25239
|
+
var booleanish = external_exports.string().optional().transform((v) => {
|
|
25240
|
+
if (v === void 0) return void 0;
|
|
25241
|
+
const t = v.trim().toLowerCase();
|
|
25242
|
+
if (t === "" || t === "false" || t === "0") return false;
|
|
25243
|
+
return true;
|
|
25244
|
+
}).catch(void 0);
|
|
25245
|
+
var optionalBaseUrl = external_exports.preprocess((v) => {
|
|
25246
|
+
if (typeof v === "string" && v.trim() === "") return void 0;
|
|
25247
|
+
return v;
|
|
25248
|
+
}, external_exports.string().optional()).catch(void 0);
|
|
25249
|
+
var providerEnvShape = {
|
|
25250
|
+
CLAUDE_CODE_USE_BEDROCK: booleanish,
|
|
25251
|
+
CLAUDE_CODE_USE_VERTEX: booleanish,
|
|
25252
|
+
ANTHROPIC_BASE_URL: optionalBaseUrl
|
|
25253
|
+
};
|
|
25254
|
+
var ProviderEnvSchema = external_exports.object(providerEnvShape);
|
|
25255
|
+
|
|
25256
|
+
// ../../packages/plugin-sdk/src/provider.ts
|
|
25257
|
+
function hostOf(url2) {
|
|
25258
|
+
try {
|
|
25259
|
+
const host = new URL(url2).host;
|
|
25260
|
+
if (host !== "") return host;
|
|
25261
|
+
} catch {
|
|
25262
|
+
}
|
|
25263
|
+
try {
|
|
25264
|
+
const host = new URL(`https://${url2}`).host;
|
|
25265
|
+
return host !== "" ? host : void 0;
|
|
25266
|
+
} catch {
|
|
25267
|
+
return void 0;
|
|
25268
|
+
}
|
|
25269
|
+
}
|
|
25270
|
+
function resolveProvider() {
|
|
25271
|
+
const parsed = ProviderEnvSchema.safeParse(process.env);
|
|
25272
|
+
const env = parsed.success ? parsed.data : ProviderEnvSchema.parse({});
|
|
25273
|
+
if (env.CLAUDE_CODE_USE_BEDROCK === true) return { provider: "bedrock" };
|
|
25274
|
+
if (env.CLAUDE_CODE_USE_VERTEX === true) return { provider: "vertex" };
|
|
25275
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
25276
|
+
if (baseUrl !== void 0 && baseUrl !== "") {
|
|
25277
|
+
const host = hostOf(baseUrl);
|
|
25278
|
+
if (host !== void 0 && host !== DEFAULT_ANTHROPIC_HOST) {
|
|
25279
|
+
return { provider: "gateway", gatewayHost: host };
|
|
25280
|
+
}
|
|
25281
|
+
}
|
|
25282
|
+
return { provider: "anthropic" };
|
|
25283
|
+
}
|
|
25284
|
+
function providerFromModelId(modelId) {
|
|
25285
|
+
const id = modelId.trim();
|
|
25286
|
+
if (id === "") return "unknown";
|
|
25287
|
+
if (/^(?:[a-z]{2,4}\.)?anthropic\./.test(id)) return "bedrock";
|
|
25288
|
+
if (id.includes("@")) return "vertex";
|
|
25289
|
+
if (id.includes("/")) return "gateway";
|
|
25290
|
+
if (id.includes(":")) return "gateway";
|
|
25291
|
+
if (id.startsWith("claude-")) return "anthropic";
|
|
25292
|
+
return "unknown";
|
|
25293
|
+
}
|
|
25294
|
+
|
|
25295
|
+
// ../../packages/plugin-sdk/src/config.ts
|
|
25296
|
+
function loadConfig(base = defaultDataDir()) {
|
|
25297
|
+
try {
|
|
25298
|
+
ensureLayoutDirSync(base);
|
|
25299
|
+
const settingsFile = join7(settingsDir(base), "settings.json");
|
|
25300
|
+
if (existsSync4(settingsFile)) tightenFile(settingsFile);
|
|
25301
|
+
} catch {
|
|
25302
|
+
}
|
|
25303
|
+
migrateLegacyLayout(base);
|
|
25304
|
+
const settings = readWorkspaceSettings(base);
|
|
23949
25305
|
return {
|
|
23950
25306
|
settings,
|
|
23951
25307
|
dataDir: dataDir(base),
|
|
@@ -23964,9 +25320,9 @@ function resolveProviderSafe() {
|
|
|
23964
25320
|
}
|
|
23965
25321
|
|
|
23966
25322
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
23967
|
-
import { readdirSync, readFileSync as
|
|
25323
|
+
import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
23968
25324
|
import { homedir as homedir2 } from "os";
|
|
23969
|
-
import { basename as basename2, join as
|
|
25325
|
+
import { basename as basename2, join as join9 } from "path";
|
|
23970
25326
|
|
|
23971
25327
|
// ../../packages/detections/src/egress/registry.ts
|
|
23972
25328
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -24754,12 +26110,12 @@ function redact(text, findings) {
|
|
|
24754
26110
|
const regions = [];
|
|
24755
26111
|
for (const f of sorted) {
|
|
24756
26112
|
const rank = SEVERITY_RANK2[f.severity];
|
|
24757
|
-
const
|
|
24758
|
-
if (
|
|
24759
|
-
|
|
24760
|
-
if (rank >
|
|
24761
|
-
|
|
24762
|
-
|
|
26113
|
+
const open2 = regions[regions.length - 1];
|
|
26114
|
+
if (open2 && f.span.start < open2.end) {
|
|
26115
|
+
open2.end = Math.max(open2.end, f.span.end);
|
|
26116
|
+
if (rank > open2.rank) {
|
|
26117
|
+
open2.rank = rank;
|
|
26118
|
+
open2.category = f.category;
|
|
24763
26119
|
}
|
|
24764
26120
|
} else {
|
|
24765
26121
|
regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
|
|
@@ -24788,6 +26144,24 @@ function maskMatch(raw) {
|
|
|
24788
26144
|
return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
|
|
24789
26145
|
}
|
|
24790
26146
|
|
|
26147
|
+
// ../../packages/detections/src/pointer-shield.ts
|
|
26148
|
+
function shieldPointers(text) {
|
|
26149
|
+
const spans = [];
|
|
26150
|
+
let out = null;
|
|
26151
|
+
for (const match of text.matchAll(pointerTokenScanner())) {
|
|
26152
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
26153
|
+
out ??= text;
|
|
26154
|
+
out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
|
|
26155
|
+
}
|
|
26156
|
+
return { text: out ?? text, spans };
|
|
26157
|
+
}
|
|
26158
|
+
function dropShieldedFindings(findings, spans) {
|
|
26159
|
+
if (spans.length === 0) return findings;
|
|
26160
|
+
return findings.filter(
|
|
26161
|
+
(finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
|
|
26162
|
+
);
|
|
26163
|
+
}
|
|
26164
|
+
|
|
24791
26165
|
// ../../packages/detections/src/posture/config-posture.ts
|
|
24792
26166
|
var RULE_VERSION = "1";
|
|
24793
26167
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -27002,7 +28376,8 @@ function scanText(text, ruleVersions) {
|
|
|
27002
28376
|
if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
|
|
27003
28377
|
try {
|
|
27004
28378
|
const rules = getLoadedRules();
|
|
27005
|
-
const
|
|
28379
|
+
const shielded = shieldPointers(text);
|
|
28380
|
+
const matches = dropShieldedFindings(scan(shielded.text, rules), shielded.spans);
|
|
27006
28381
|
if (matches.length === 0) return { masked: text, findings: [] };
|
|
27007
28382
|
const byId = new Map(rules.map((r) => [r.id, r]));
|
|
27008
28383
|
const findings = matches.map((m) => {
|
|
@@ -27025,8 +28400,8 @@ function scanText(text, ruleVersions) {
|
|
|
27025
28400
|
}
|
|
27026
28401
|
|
|
27027
28402
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
27028
|
-
import { existsSync as existsSync5, readFileSync as
|
|
27029
|
-
import { basename, dirname, isAbsolute, join as
|
|
28403
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
28404
|
+
import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
|
|
27030
28405
|
function resolveRepoIdentity(cwd) {
|
|
27031
28406
|
try {
|
|
27032
28407
|
const root = findGitRoot(cwd);
|
|
@@ -27059,36 +28434,36 @@ function resolveRepoNwo(cwd) {
|
|
|
27059
28434
|
function findGitRoot(start) {
|
|
27060
28435
|
let dir = start;
|
|
27061
28436
|
for (; ; ) {
|
|
27062
|
-
if (existsSync5(
|
|
28437
|
+
if (existsSync5(join8(dir, ".git"))) return dir;
|
|
27063
28438
|
const parent = dirname(dir);
|
|
27064
28439
|
if (parent === dir) return void 0;
|
|
27065
28440
|
dir = parent;
|
|
27066
28441
|
}
|
|
27067
28442
|
}
|
|
27068
28443
|
function resolveGitContext(root) {
|
|
27069
|
-
const dotGit =
|
|
28444
|
+
const dotGit = join8(root, ".git");
|
|
27070
28445
|
try {
|
|
27071
|
-
if (
|
|
27072
|
-
return { configPath:
|
|
28446
|
+
if (statSync2(dotGit).isDirectory()) {
|
|
28447
|
+
return { configPath: join8(dotGit, "config"), headRoot: root };
|
|
27073
28448
|
}
|
|
27074
28449
|
} catch {
|
|
27075
28450
|
return void 0;
|
|
27076
28451
|
}
|
|
27077
28452
|
const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
|
|
27078
28453
|
if (!target) return void 0;
|
|
27079
|
-
const gitdir = isAbsolute(target) ? target :
|
|
27080
|
-
if (existsSync5(
|
|
27081
|
-
return { configPath:
|
|
28454
|
+
const gitdir = isAbsolute(target) ? target : join8(root, target);
|
|
28455
|
+
if (existsSync5(join8(gitdir, "config"))) {
|
|
28456
|
+
return { configPath: join8(gitdir, "config"), headRoot: root };
|
|
27082
28457
|
}
|
|
27083
|
-
const commonRaw = safeRead(
|
|
28458
|
+
const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
|
|
27084
28459
|
if (!commonRaw) return void 0;
|
|
27085
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
28460
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
|
|
27086
28461
|
const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
|
|
27087
|
-
return { configPath:
|
|
28462
|
+
return { configPath: join8(commonGitDir, "config"), headRoot };
|
|
27088
28463
|
}
|
|
27089
28464
|
function safeRead(path) {
|
|
27090
28465
|
try {
|
|
27091
|
-
return
|
|
28466
|
+
return readFileSync4(path, "utf8");
|
|
27092
28467
|
} catch {
|
|
27093
28468
|
return void 0;
|
|
27094
28469
|
}
|
|
@@ -27139,13 +28514,13 @@ function nwoFromUrl(url2) {
|
|
|
27139
28514
|
}
|
|
27140
28515
|
|
|
27141
28516
|
// ../../packages/plugin-sdk/src/events.ts
|
|
27142
|
-
import { createHash as createHash4, randomUUID as
|
|
28517
|
+
import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
|
|
27143
28518
|
function contentHashOf(text) {
|
|
27144
28519
|
return createHash4("sha256").update(text).digest("hex");
|
|
27145
28520
|
}
|
|
27146
28521
|
function buildIngestEvent(input) {
|
|
27147
28522
|
return {
|
|
27148
|
-
id:
|
|
28523
|
+
id: randomUUID11(),
|
|
27149
28524
|
sourceTool: input.sourceTool,
|
|
27150
28525
|
kind: input.kind,
|
|
27151
28526
|
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -27156,7 +28531,7 @@ function buildIngestEvent(input) {
|
|
|
27156
28531
|
// SDK boot in the fail-open hook path). Preserve any id the caller already set.
|
|
27157
28532
|
metadata: {
|
|
27158
28533
|
...input.metadata,
|
|
27159
|
-
correlationId: input.metadata?.correlationId ??
|
|
28534
|
+
correlationId: input.metadata?.correlationId ?? randomUUID11()
|
|
27160
28535
|
}
|
|
27161
28536
|
};
|
|
27162
28537
|
}
|
|
@@ -27191,8 +28566,8 @@ function resolveInventoryContext(input) {
|
|
|
27191
28566
|
}
|
|
27192
28567
|
|
|
27193
28568
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
27194
|
-
import { mkdirSync as
|
|
27195
|
-
import { join as
|
|
28569
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
28570
|
+
import { join as join10 } from "path";
|
|
27196
28571
|
|
|
27197
28572
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
27198
28573
|
import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
|
|
@@ -27200,8 +28575,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
|
|
|
27200
28575
|
|
|
27201
28576
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
27202
28577
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
27203
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as
|
|
27204
|
-
import { basename as basename4, join as
|
|
28578
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
28579
|
+
import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
|
|
27205
28580
|
|
|
27206
28581
|
// ../../packages/plugin-sdk/src/raw-egress.ts
|
|
27207
28582
|
var RawEgressError = class extends Error {
|
|
@@ -27297,7 +28672,7 @@ async function filterUnsafeRules(rules, gateway, opts) {
|
|
|
27297
28672
|
}
|
|
27298
28673
|
|
|
27299
28674
|
// ../../packages/plugin-sdk/src/runtime.ts
|
|
27300
|
-
import { randomUUID as
|
|
28675
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
27301
28676
|
var ENFORCEMENT_CEILING_ENABLED = false;
|
|
27302
28677
|
var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
|
|
27303
28678
|
function entryIsActive(entry, now) {
|
|
@@ -27400,7 +28775,12 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27400
28775
|
if (worst === "block") return { action: "block", text: null, findings };
|
|
27401
28776
|
if (worst === "redact") {
|
|
27402
28777
|
const redactFindings = findings.filter((f) => actionFor(f) === "redact");
|
|
27403
|
-
return {
|
|
28778
|
+
return {
|
|
28779
|
+
action: "redact",
|
|
28780
|
+
text: redact(text, redactFindings),
|
|
28781
|
+
findings,
|
|
28782
|
+
enforcedFindings: redactFindings
|
|
28783
|
+
};
|
|
27404
28784
|
}
|
|
27405
28785
|
return { action: worst, text, findings };
|
|
27406
28786
|
}
|
|
@@ -27441,9 +28821,17 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27441
28821
|
else groups.set(pair, [finding]);
|
|
27442
28822
|
}
|
|
27443
28823
|
const now = Date.now();
|
|
28824
|
+
const preAuthorized = new Set(ctx.preAuthorizedGrantIds ?? []);
|
|
27444
28825
|
for (const [pair, group] of groups) {
|
|
27445
28826
|
const entry = entries.get(pair);
|
|
27446
|
-
if (!entry
|
|
28827
|
+
if (!entry) continue;
|
|
28828
|
+
if (preAuthorized.has(entry.id)) {
|
|
28829
|
+
if (!conditionsMatch(entry.conditions, ctx)) continue;
|
|
28830
|
+
for (const finding of group) excepted.add(finding);
|
|
28831
|
+
exceptionIds.push(entry.id);
|
|
28832
|
+
continue;
|
|
28833
|
+
}
|
|
28834
|
+
if (!entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
|
|
27447
28835
|
continue;
|
|
27448
28836
|
}
|
|
27449
28837
|
let consumed = false;
|
|
@@ -27475,7 +28863,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27475
28863
|
const pair = `${finding.ruleId}:${fp}`;
|
|
27476
28864
|
if (seen.has(pair)) continue;
|
|
27477
28865
|
seen.add(pair);
|
|
27478
|
-
const reference =
|
|
28866
|
+
const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
|
|
27479
28867
|
const maskedValue = maskMatch(finding.rawMatch);
|
|
27480
28868
|
try {
|
|
27481
28869
|
await gateway.recordBlockedDetection({
|
|
@@ -27499,7 +28887,8 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27499
28887
|
async function evaluate(text, context, ctx) {
|
|
27500
28888
|
try {
|
|
27501
28889
|
await ensureInitialized();
|
|
27502
|
-
const
|
|
28890
|
+
const shielded = shieldPointers(text);
|
|
28891
|
+
const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
|
|
27503
28892
|
const fpCache = /* @__PURE__ */ new Map();
|
|
27504
28893
|
const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
|
|
27505
28894
|
const decision = decide(findings, text, excepted);
|
|
@@ -27522,7 +28911,11 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27522
28911
|
const { decision, excepted, exceptionIds } = await evaluate(
|
|
27523
28912
|
input.text,
|
|
27524
28913
|
filePath ? { filePath } : void 0,
|
|
27525
|
-
{
|
|
28914
|
+
{
|
|
28915
|
+
sourceTool: input.sourceTool,
|
|
28916
|
+
metadata: input.metadata,
|
|
28917
|
+
preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
|
|
28918
|
+
}
|
|
27526
28919
|
);
|
|
27527
28920
|
if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
|
|
27528
28921
|
try {
|
|
@@ -27552,7 +28945,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27552
28945
|
valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
|
|
27553
28946
|
}) : void 0;
|
|
27554
28947
|
return {
|
|
27555
|
-
id:
|
|
28948
|
+
id: randomUUID12(),
|
|
27556
28949
|
eventId: event.id,
|
|
27557
28950
|
ruleId: match.ruleId,
|
|
27558
28951
|
category: match.category,
|
|
@@ -27578,7 +28971,7 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27578
28971
|
const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
|
|
27579
28972
|
return contentHashOf(JSON.stringify(sorted));
|
|
27580
28973
|
} catch {
|
|
27581
|
-
return `unresolved-${
|
|
28974
|
+
return `unresolved-${randomUUID12()}`;
|
|
27582
28975
|
}
|
|
27583
28976
|
}
|
|
27584
28977
|
async function close() {
|
|
@@ -27591,11 +28984,306 @@ function createPluginRuntime(gateway, settings, opts) {
|
|
|
27591
28984
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
27592
28985
|
|
|
27593
28986
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
27594
|
-
import { mkdirSync as
|
|
27595
|
-
import { join as
|
|
28987
|
+
import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
28988
|
+
import { join as join12 } from "path";
|
|
28989
|
+
|
|
28990
|
+
// ../../packages/plugin-sdk/src/tokenize.ts
|
|
28991
|
+
function redactedPlaceholder(category) {
|
|
28992
|
+
return `[REDACTED:${category.toUpperCase()}]`;
|
|
28993
|
+
}
|
|
28994
|
+
var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
|
|
28995
|
+
var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
28996
|
+
function groupSpans(text, findings) {
|
|
28997
|
+
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);
|
|
28998
|
+
const groups = [];
|
|
28999
|
+
for (const finding of sorted) {
|
|
29000
|
+
const last = groups[groups.length - 1];
|
|
29001
|
+
if (last && finding.span.start < last.end) {
|
|
29002
|
+
last.end = Math.max(last.end, finding.span.end);
|
|
29003
|
+
if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
|
|
29004
|
+
last.category = finding.category;
|
|
29005
|
+
last.severity = finding.severity;
|
|
29006
|
+
}
|
|
29007
|
+
delete last.finding;
|
|
29008
|
+
continue;
|
|
29009
|
+
}
|
|
29010
|
+
groups.push({
|
|
29011
|
+
start: finding.span.start,
|
|
29012
|
+
end: finding.span.end,
|
|
29013
|
+
finding,
|
|
29014
|
+
category: finding.category,
|
|
29015
|
+
severity: finding.severity
|
|
29016
|
+
});
|
|
29017
|
+
}
|
|
29018
|
+
return groups;
|
|
29019
|
+
}
|
|
29020
|
+
var NULL_RESOLVER = () => Promise.resolve(null);
|
|
29021
|
+
var SecretVaultGlue = class {
|
|
29022
|
+
#vault;
|
|
29023
|
+
revealGrantResolver;
|
|
29024
|
+
// Set only when THIS glue opened the store, so a glue over an injected vault
|
|
29025
|
+
// never closes a handle it does not own.
|
|
29026
|
+
#release;
|
|
29027
|
+
constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
|
|
29028
|
+
this.#vault = vault;
|
|
29029
|
+
this.revealGrantResolver = revealGrantResolver;
|
|
29030
|
+
this.#release = release2;
|
|
29031
|
+
}
|
|
29032
|
+
close() {
|
|
29033
|
+
const release2 = this.#release;
|
|
29034
|
+
this.#release = void 0;
|
|
29035
|
+
try {
|
|
29036
|
+
release2?.();
|
|
29037
|
+
} catch {
|
|
29038
|
+
}
|
|
29039
|
+
}
|
|
29040
|
+
async tokenizeValue(raw, meta3) {
|
|
29041
|
+
try {
|
|
29042
|
+
const result = await this.#vault.tokenize(raw, meta3);
|
|
29043
|
+
return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
|
|
29044
|
+
} catch {
|
|
29045
|
+
return redactedPlaceholder(meta3.category);
|
|
29046
|
+
}
|
|
29047
|
+
}
|
|
29048
|
+
async tokenizeText(text, opts) {
|
|
29049
|
+
try {
|
|
29050
|
+
const findings = opts?.findings ?? this.#selfScan(text);
|
|
29051
|
+
if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
29052
|
+
if (findings.length === 0) return { text, pointers: [], degraded: [] };
|
|
29053
|
+
const groups = groupSpans(text, findings);
|
|
29054
|
+
const pointers = [];
|
|
29055
|
+
const degraded = [];
|
|
29056
|
+
let out = text;
|
|
29057
|
+
for (const group of [...groups].reverse()) {
|
|
29058
|
+
const original = text.slice(group.start, group.end);
|
|
29059
|
+
const finding = group.finding;
|
|
29060
|
+
let replacement;
|
|
29061
|
+
if (finding === void 0) {
|
|
29062
|
+
replacement = redactedPlaceholder(group.category);
|
|
29063
|
+
degraded.unshift({ category: group.category });
|
|
29064
|
+
} else if (original !== finding.rawMatch) {
|
|
29065
|
+
replacement = redactedPlaceholder(group.category);
|
|
29066
|
+
degraded.unshift({ category: group.category });
|
|
29067
|
+
} else {
|
|
29068
|
+
replacement = await this.tokenizeValue(finding.rawMatch, {
|
|
29069
|
+
ruleId: finding.ruleId,
|
|
29070
|
+
category: finding.category,
|
|
29071
|
+
maskedMatch: maskMatch(finding.rawMatch)
|
|
29072
|
+
});
|
|
29073
|
+
if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
|
|
29074
|
+
else degraded.unshift({ category: finding.category });
|
|
29075
|
+
}
|
|
29076
|
+
out = out.slice(0, group.start) + replacement + out.slice(group.end);
|
|
29077
|
+
}
|
|
29078
|
+
if (opts?.sighting && pointers.length > 0) {
|
|
29079
|
+
for (const pointer of pointers) {
|
|
29080
|
+
try {
|
|
29081
|
+
const id = pointer.split(".")[1];
|
|
29082
|
+
if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
|
|
29083
|
+
} catch {
|
|
29084
|
+
}
|
|
29085
|
+
}
|
|
29086
|
+
}
|
|
29087
|
+
return { text: out, pointers, degraded };
|
|
29088
|
+
} catch {
|
|
29089
|
+
return { text: "[REDACTED]", pointers: [], degraded: [] };
|
|
29090
|
+
}
|
|
29091
|
+
}
|
|
29092
|
+
async detokenizeText(text, opts) {
|
|
29093
|
+
try {
|
|
29094
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
29095
|
+
if (matches.length === 0) return { text, revealed: 0 };
|
|
29096
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
29097
|
+
for (const match of matches) {
|
|
29098
|
+
occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
|
|
29099
|
+
}
|
|
29100
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
29101
|
+
for (const [pointer, count] of occurrences) {
|
|
29102
|
+
try {
|
|
29103
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
29104
|
+
target: "human",
|
|
29105
|
+
reason: opts.reason,
|
|
29106
|
+
pointerCount: count
|
|
29107
|
+
});
|
|
29108
|
+
resolved.set(pointer, typeof value === "string" ? value : null);
|
|
29109
|
+
} catch {
|
|
29110
|
+
resolved.set(pointer, null);
|
|
29111
|
+
}
|
|
29112
|
+
}
|
|
29113
|
+
let out = text;
|
|
29114
|
+
let revealed = 0;
|
|
29115
|
+
for (const match of [...matches].reverse()) {
|
|
29116
|
+
const value = resolved.get(match[0]);
|
|
29117
|
+
const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
|
|
29118
|
+
if (value !== null && value !== void 0) revealed += 1;
|
|
29119
|
+
out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
|
|
29120
|
+
}
|
|
29121
|
+
return { text: out, revealed };
|
|
29122
|
+
} catch {
|
|
29123
|
+
return { text, revealed: 0 };
|
|
29124
|
+
}
|
|
29125
|
+
}
|
|
29126
|
+
// Scan with the bundled packs, as the mask path does. Pointers already in the
|
|
29127
|
+
// text are blanked first so a pointer is never re-tokenized. Returns null
|
|
29128
|
+
// when the registry or the scan itself failed — the caller must then treat
|
|
29129
|
+
// the whole text as unclassifiable.
|
|
29130
|
+
#selfScan(text) {
|
|
29131
|
+
try {
|
|
29132
|
+
registerBundledPacks();
|
|
29133
|
+
const shielded = shieldPointers(text);
|
|
29134
|
+
return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
|
|
29135
|
+
} catch {
|
|
29136
|
+
return null;
|
|
29137
|
+
}
|
|
29138
|
+
}
|
|
29139
|
+
async describePointerSafe(token) {
|
|
29140
|
+
try {
|
|
29141
|
+
return await this.#vault.describePointer(token);
|
|
29142
|
+
} catch {
|
|
29143
|
+
return null;
|
|
29144
|
+
}
|
|
29145
|
+
}
|
|
29146
|
+
async probeModelPointers(text, opts) {
|
|
29147
|
+
const granted = /* @__PURE__ */ new Map();
|
|
29148
|
+
const ungranted = [];
|
|
29149
|
+
try {
|
|
29150
|
+
for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
|
|
29151
|
+
try {
|
|
29152
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29153
|
+
if (grantId === null) ungranted.push(pointer);
|
|
29154
|
+
else granted.set(pointer, grantId);
|
|
29155
|
+
} catch {
|
|
29156
|
+
ungranted.push(pointer);
|
|
29157
|
+
}
|
|
29158
|
+
}
|
|
29159
|
+
return { granted, ungranted };
|
|
29160
|
+
} catch {
|
|
29161
|
+
return { granted: /* @__PURE__ */ new Map(), ungranted };
|
|
29162
|
+
}
|
|
29163
|
+
}
|
|
29164
|
+
async substituteModelPointers(text, opts) {
|
|
29165
|
+
try {
|
|
29166
|
+
const matches = [...text.matchAll(pointerTokenScanner())];
|
|
29167
|
+
if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29168
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
29169
|
+
for (const pointer of new Set(matches.map((m) => m[0]))) {
|
|
29170
|
+
try {
|
|
29171
|
+
const grantId = await opts.resolveGrant(pointer);
|
|
29172
|
+
if (grantId === null) {
|
|
29173
|
+
await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
|
|
29174
|
+
resolved.set(pointer, null);
|
|
29175
|
+
continue;
|
|
29176
|
+
}
|
|
29177
|
+
const value = await this.#vault.detokenize(pointer, {
|
|
29178
|
+
target: "model",
|
|
29179
|
+
reason: "model-input",
|
|
29180
|
+
grantId
|
|
29181
|
+
});
|
|
29182
|
+
resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
|
|
29183
|
+
} catch {
|
|
29184
|
+
resolved.set(pointer, null);
|
|
29185
|
+
}
|
|
29186
|
+
}
|
|
29187
|
+
const spentGrants = /* @__PURE__ */ new Set();
|
|
29188
|
+
for (const entry of resolved.values()) {
|
|
29189
|
+
if (entry === null || spentGrants.has(entry.grantId)) continue;
|
|
29190
|
+
spentGrants.add(entry.grantId);
|
|
29191
|
+
try {
|
|
29192
|
+
await this.#vault.consumeGrant?.(entry.grantId);
|
|
29193
|
+
} catch {
|
|
29194
|
+
}
|
|
29195
|
+
}
|
|
29196
|
+
let out = text;
|
|
29197
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
29198
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
29199
|
+
for (const match of [...matches].reverse()) {
|
|
29200
|
+
const entry = resolved.get(match[0]);
|
|
29201
|
+
if (entry === null || entry === void 0) {
|
|
29202
|
+
unresolved.add(match[0]);
|
|
29203
|
+
continue;
|
|
29204
|
+
}
|
|
29205
|
+
revealed.add(match[0]);
|
|
29206
|
+
out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
|
|
29207
|
+
}
|
|
29208
|
+
return {
|
|
29209
|
+
text: out,
|
|
29210
|
+
revealed: [...revealed],
|
|
29211
|
+
unresolved: [...unresolved],
|
|
29212
|
+
grantIds: [...spentGrants]
|
|
29213
|
+
};
|
|
29214
|
+
} catch {
|
|
29215
|
+
return { text, revealed: [], unresolved: [], grantIds: [] };
|
|
29216
|
+
}
|
|
29217
|
+
}
|
|
29218
|
+
};
|
|
29219
|
+
function createVaultGlue(options) {
|
|
29220
|
+
if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
|
|
29221
|
+
const base = options?.base ?? defaultDataDir();
|
|
29222
|
+
try {
|
|
29223
|
+
const dir = dataDir(base);
|
|
29224
|
+
const db = openLocalDatabase(dir);
|
|
29225
|
+
const settings = readWorkspaceSettings(base);
|
|
29226
|
+
const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
|
|
29227
|
+
const vault = new SecretVault({
|
|
29228
|
+
repo: db.secretVault,
|
|
29229
|
+
keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
|
|
29230
|
+
fingerprintKey: loadOrCreateFingerprintKey(dir),
|
|
29231
|
+
// Read live so a revocation applies to the very next call, not the next
|
|
29232
|
+
// process.
|
|
29233
|
+
isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
|
|
29234
|
+
// This is the one construction site that reveals to the model, so it is
|
|
29235
|
+
// the one that supplies the last gate. The decision is re-taken from the
|
|
29236
|
+
// ROW's identity at the moment of crossing, which closes the window
|
|
29237
|
+
// between resolving a grant and spending it: a grant revoked in between
|
|
29238
|
+
// refuses here.
|
|
29239
|
+
//
|
|
29240
|
+
// The re-decision is on the identity alone, never on the grant id
|
|
29241
|
+
// matching the one the resolver returned. ExceptionPolicyProvider
|
|
29242
|
+
// promises no id stability across calls — a provider deciding from
|
|
29243
|
+
// external policy may well mint a fresh id each time — so comparing ids
|
|
29244
|
+
// would silently refuse every crossing for such a provider while looking
|
|
29245
|
+
// like a security check. `allow` for this row is the whole question.
|
|
29246
|
+
verifyGrant: async (_grantId, identity) => {
|
|
29247
|
+
const decision = await provider.decideReveal(identity);
|
|
29248
|
+
return decision.allow;
|
|
29249
|
+
}
|
|
29250
|
+
});
|
|
29251
|
+
const vaultWithSightings = {
|
|
29252
|
+
tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
|
|
29253
|
+
detokenize: (token, opts) => vault.detokenize(token, opts),
|
|
29254
|
+
describePointer: (token) => vault.describePointer(token),
|
|
29255
|
+
resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
|
|
29256
|
+
recordSighting: (pointerId, sighting) => {
|
|
29257
|
+
db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
|
|
29258
|
+
},
|
|
29259
|
+
consumeGrant: (grantId) => db.exceptions.consume(grantId)
|
|
29260
|
+
};
|
|
29261
|
+
const revealGrantResolver = async (pointer) => {
|
|
29262
|
+
try {
|
|
29263
|
+
const identity = await vault.resolvePointerIdentity(pointer);
|
|
29264
|
+
if (identity === null) return null;
|
|
29265
|
+
const decision = await provider.decideReveal(identity);
|
|
29266
|
+
return decision.allow ? decision.grantId : null;
|
|
29267
|
+
} catch {
|
|
29268
|
+
return null;
|
|
29269
|
+
}
|
|
29270
|
+
};
|
|
29271
|
+
return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
|
|
29272
|
+
db.close();
|
|
29273
|
+
});
|
|
29274
|
+
} catch {
|
|
29275
|
+
return new SecretVaultGlue(UNOPENABLE_VAULT);
|
|
29276
|
+
}
|
|
29277
|
+
}
|
|
29278
|
+
var UNOPENABLE_VAULT = {
|
|
29279
|
+
tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29280
|
+
detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
|
|
29281
|
+
describePointer: () => Promise.resolve(null),
|
|
29282
|
+
resolvePointerIdentity: () => Promise.resolve(null)
|
|
29283
|
+
};
|
|
27596
29284
|
|
|
27597
29285
|
// ../../packages/plugin-runtime/src/standalone-gateway.ts
|
|
27598
|
-
import { randomUUID as
|
|
29286
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
27599
29287
|
|
|
27600
29288
|
// ../../packages/plugin-runtime/src/recorder.ts
|
|
27601
29289
|
var PLUGIN_RECORDER_BINARY = "plugin";
|
|
@@ -27634,12 +29322,12 @@ var StandaloneDataGateway = class {
|
|
|
27634
29322
|
// reconciler drops the whole pass and recovers it idempotently on the next read.
|
|
27635
29323
|
recordLlmCalls(inputs) {
|
|
27636
29324
|
if (inputs.length === 0) return Promise.resolve();
|
|
27637
|
-
return new Promise((
|
|
29325
|
+
return new Promise((resolve2, reject) => {
|
|
27638
29326
|
try {
|
|
27639
29327
|
this.db.auditEvents.runInTransaction(() => {
|
|
27640
29328
|
for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
|
|
27641
29329
|
});
|
|
27642
|
-
|
|
29330
|
+
resolve2();
|
|
27643
29331
|
} catch (err) {
|
|
27644
29332
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
27645
29333
|
}
|
|
@@ -27651,12 +29339,12 @@ var StandaloneDataGateway = class {
|
|
|
27651
29339
|
// drops the whole pass and recovers it idempotently next time.
|
|
27652
29340
|
recordToolCalls(inputs) {
|
|
27653
29341
|
if (inputs.length === 0) return Promise.resolve();
|
|
27654
|
-
return new Promise((
|
|
29342
|
+
return new Promise((resolve2, reject) => {
|
|
27655
29343
|
try {
|
|
27656
29344
|
this.db.auditEvents.runInTransaction(() => {
|
|
27657
29345
|
for (const input of inputs) this.writeToolCall(input);
|
|
27658
29346
|
});
|
|
27659
|
-
|
|
29347
|
+
resolve2();
|
|
27660
29348
|
} catch (err) {
|
|
27661
29349
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
27662
29350
|
}
|
|
@@ -27757,7 +29445,7 @@ var StandaloneDataGateway = class {
|
|
|
27757
29445
|
const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
|
|
27758
29446
|
const installed = this.installedScanRules();
|
|
27759
29447
|
const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
|
|
27760
|
-
id:
|
|
29448
|
+
id: randomUUID13(),
|
|
27761
29449
|
scope: "global",
|
|
27762
29450
|
target: { ruleId },
|
|
27763
29451
|
action,
|
|
@@ -27910,15 +29598,15 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
|
|
|
27910
29598
|
}
|
|
27911
29599
|
|
|
27912
29600
|
// ../../packages/plugin-runtime/src/handle-session-start.ts
|
|
27913
|
-
import { randomUUID as
|
|
29601
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
27914
29602
|
var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
27915
29603
|
|
|
27916
29604
|
// src/history/transcripts.ts
|
|
27917
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
29605
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
27918
29606
|
import { homedir as homedir3 } from "os";
|
|
27919
|
-
import { join as
|
|
29607
|
+
import { join as join13 } from "path";
|
|
27920
29608
|
function transcriptsDir(home) {
|
|
27921
|
-
return
|
|
29609
|
+
return join13(home ?? homedir3(), ".claude", "projects");
|
|
27922
29610
|
}
|
|
27923
29611
|
function isRecord(value) {
|
|
27924
29612
|
return typeof value === "object" && value !== null;
|
|
@@ -28166,7 +29854,7 @@ function* iterateFileContents(dir, excludeSessionId) {
|
|
|
28166
29854
|
return;
|
|
28167
29855
|
}
|
|
28168
29856
|
for (const project of projects) {
|
|
28169
|
-
const projectDir =
|
|
29857
|
+
const projectDir = join13(dir, project);
|
|
28170
29858
|
let files;
|
|
28171
29859
|
try {
|
|
28172
29860
|
files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
|
|
@@ -28176,10 +29864,10 @@ function* iterateFileContents(dir, excludeSessionId) {
|
|
|
28176
29864
|
for (const file2 of files) {
|
|
28177
29865
|
if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
|
|
28178
29866
|
continue;
|
|
28179
|
-
const filePath =
|
|
29867
|
+
const filePath = join13(projectDir, file2);
|
|
28180
29868
|
let content;
|
|
28181
29869
|
try {
|
|
28182
|
-
content =
|
|
29870
|
+
content = readFileSync8(filePath, "utf8");
|
|
28183
29871
|
} catch {
|
|
28184
29872
|
continue;
|
|
28185
29873
|
}
|
|
@@ -28243,7 +29931,15 @@ function buildTriageHit(text, f, otherFindings = [], filePath = "") {
|
|
|
28243
29931
|
async function scanHistory(config2, opts = {}, onHit) {
|
|
28244
29932
|
const windowDays = opts.windowDays ?? 30;
|
|
28245
29933
|
if (config2.settings.historicalAccess !== "full") {
|
|
28246
|
-
return {
|
|
29934
|
+
return {
|
|
29935
|
+
consented: false,
|
|
29936
|
+
scanned: 0,
|
|
29937
|
+
skipped: 0,
|
|
29938
|
+
findings: 0,
|
|
29939
|
+
bySeverity: {},
|
|
29940
|
+
windowDays,
|
|
29941
|
+
visitedFiles: []
|
|
29942
|
+
};
|
|
28247
29943
|
}
|
|
28248
29944
|
const gateway = resolveDataGateway(config2);
|
|
28249
29945
|
const runtime = createPluginRuntime(gateway, config2.settings, { dataDir: config2.dataDir });
|
|
@@ -28251,9 +29947,11 @@ async function scanHistory(config2, opts = {}, onHit) {
|
|
|
28251
29947
|
let scanned = 0;
|
|
28252
29948
|
let skipped = 0;
|
|
28253
29949
|
let findings = 0;
|
|
29950
|
+
const visited = /* @__PURE__ */ new Set();
|
|
28254
29951
|
try {
|
|
28255
29952
|
const seen = await gateway.knownContentHashes();
|
|
28256
29953
|
for (const message of iterateHistory(opts)) {
|
|
29954
|
+
visited.add(message.filePath);
|
|
28257
29955
|
const hash2 = contentHashOf(message.text);
|
|
28258
29956
|
if (seen.has(hash2)) {
|
|
28259
29957
|
skipped++;
|
|
@@ -28289,7 +29987,85 @@ async function scanHistory(config2, opts = {}, onHit) {
|
|
|
28289
29987
|
} finally {
|
|
28290
29988
|
await runtime.close();
|
|
28291
29989
|
}
|
|
28292
|
-
return {
|
|
29990
|
+
return {
|
|
29991
|
+
consented: true,
|
|
29992
|
+
scanned,
|
|
29993
|
+
skipped,
|
|
29994
|
+
findings,
|
|
29995
|
+
bySeverity,
|
|
29996
|
+
windowDays,
|
|
29997
|
+
visitedFiles: [...visited]
|
|
29998
|
+
};
|
|
29999
|
+
}
|
|
30000
|
+
|
|
30001
|
+
// src/history/tail-scrub.ts
|
|
30002
|
+
import { readFileSync as readFileSync10, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
30003
|
+
|
|
30004
|
+
// src/remediation/redact.ts
|
|
30005
|
+
import { readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
30006
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
|
|
30007
|
+
function platformRedactionScope(home) {
|
|
30008
|
+
return { artifactRoots: [transcriptsDir(home)] };
|
|
30009
|
+
}
|
|
30010
|
+
function realPathOrNull(path) {
|
|
30011
|
+
try {
|
|
30012
|
+
return realpathSync3(path);
|
|
30013
|
+
} catch {
|
|
30014
|
+
return null;
|
|
30015
|
+
}
|
|
30016
|
+
}
|
|
30017
|
+
function isWithinRoot(realTarget, root) {
|
|
30018
|
+
const realRoot = realPathOrNull(root);
|
|
30019
|
+
if (realRoot === null) return false;
|
|
30020
|
+
const rel = relative2(realRoot, realTarget);
|
|
30021
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
30022
|
+
}
|
|
30023
|
+
function resolveRedactableArtifact(filePath, scope) {
|
|
30024
|
+
const realTarget = realPathOrNull(resolve(filePath));
|
|
30025
|
+
if (realTarget === null) return null;
|
|
30026
|
+
return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
|
|
30027
|
+
}
|
|
30028
|
+
|
|
30029
|
+
// src/history/tail-scrub.ts
|
|
30030
|
+
var DEFAULT_MAX_SCRUB_BYTES = 32 * 1024 * 1024;
|
|
30031
|
+
async function scrubTranscriptTail(filePath, deps) {
|
|
30032
|
+
try {
|
|
30033
|
+
const realPath = resolveRedactableArtifact(filePath, deps.scope);
|
|
30034
|
+
if (realPath === null) return null;
|
|
30035
|
+
const statBefore = statSync5(realPath);
|
|
30036
|
+
if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
|
|
30037
|
+
const content = readFileSync10(realPath, "utf8");
|
|
30038
|
+
const lines = content.split("\n");
|
|
30039
|
+
let rewritten = 0;
|
|
30040
|
+
for (const [i, line] of lines.entries()) {
|
|
30041
|
+
if (line === "") continue;
|
|
30042
|
+
const result = await deps.tokenizeText(line);
|
|
30043
|
+
if (result.text === line) continue;
|
|
30044
|
+
if (result.pointers.length === 0 && result.degraded.length === 0) return null;
|
|
30045
|
+
lines[i] = result.text;
|
|
30046
|
+
rewritten += 1;
|
|
30047
|
+
}
|
|
30048
|
+
if (rewritten === 0) return { rewritten: 0 };
|
|
30049
|
+
const tmpPath = `${realPath}.aka-scrub.tmp`;
|
|
30050
|
+
try {
|
|
30051
|
+
writeFileSync7(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
|
|
30052
|
+
const statNow = statSync5(realPath);
|
|
30053
|
+
if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
|
|
30054
|
+
rmSync5(tmpPath, { force: true, recursive: true });
|
|
30055
|
+
return null;
|
|
30056
|
+
}
|
|
30057
|
+
renameSync6(tmpPath, realPath);
|
|
30058
|
+
} catch {
|
|
30059
|
+
try {
|
|
30060
|
+
rmSync5(tmpPath, { force: true, recursive: true });
|
|
30061
|
+
} catch {
|
|
30062
|
+
}
|
|
30063
|
+
return null;
|
|
30064
|
+
}
|
|
30065
|
+
return { rewritten };
|
|
30066
|
+
} catch {
|
|
30067
|
+
return null;
|
|
30068
|
+
}
|
|
28293
30069
|
}
|
|
28294
30070
|
|
|
28295
30071
|
// src/history/tail.ts
|
|
@@ -28297,13 +30073,13 @@ import { createHash as createHash5 } from "crypto";
|
|
|
28297
30073
|
import {
|
|
28298
30074
|
closeSync,
|
|
28299
30075
|
fstatSync,
|
|
28300
|
-
mkdirSync as
|
|
30076
|
+
mkdirSync as mkdirSync5,
|
|
28301
30077
|
openSync,
|
|
28302
|
-
readFileSync as
|
|
30078
|
+
readFileSync as readFileSync11,
|
|
28303
30079
|
readSync,
|
|
28304
|
-
writeFileSync as
|
|
30080
|
+
writeFileSync as writeFileSync8
|
|
28305
30081
|
} from "fs";
|
|
28306
|
-
import { join as
|
|
30082
|
+
import { join as join14 } from "path";
|
|
28307
30083
|
|
|
28308
30084
|
// src/history/usage.ts
|
|
28309
30085
|
var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
|
|
@@ -28608,6 +30384,20 @@ async function runBackfill(deps) {
|
|
|
28608
30384
|
await deps.reconcileHistory(cfg);
|
|
28609
30385
|
} catch {
|
|
28610
30386
|
}
|
|
30387
|
+
let scrubbedFiles = 0;
|
|
30388
|
+
if (isVaultConsentValid(cfg.settings.vaultConsent) && summary.visitedFiles.length > 0) {
|
|
30389
|
+
try {
|
|
30390
|
+
const scrubFile = deps.scrubFile ?? buildTranscriptScrubber();
|
|
30391
|
+
for (const file2 of summary.visitedFiles) {
|
|
30392
|
+
try {
|
|
30393
|
+
const result = await scrubFile(file2);
|
|
30394
|
+
if (result !== null && result.rewritten > 0) scrubbedFiles += 1;
|
|
30395
|
+
} catch {
|
|
30396
|
+
}
|
|
30397
|
+
}
|
|
30398
|
+
} catch {
|
|
30399
|
+
}
|
|
30400
|
+
}
|
|
28611
30401
|
if (triage) {
|
|
28612
30402
|
const status = summary.scanned === 0 && summary.skipped === 0 ? "complete:no-history" : "complete";
|
|
28613
30403
|
io.stdout(triageSentinel(count, status));
|
|
@@ -28615,7 +30405,16 @@ async function runBackfill(deps) {
|
|
|
28615
30405
|
const heading = "\u2713 Historical scan complete";
|
|
28616
30406
|
const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
|
|
28617
30407
|
const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
|
|
28618
|
-
|
|
30408
|
+
const lines = [heading, "", indent(scope), "", indent(result)];
|
|
30409
|
+
if (scrubbedFiles > 0) {
|
|
30410
|
+
lines.push(
|
|
30411
|
+
"",
|
|
30412
|
+
indent(
|
|
30413
|
+
`Rewrote secrets in ${String(scrubbedFiles)} transcript file${scrubbedFiles === 1 ? "" : "s"} to recoverable vault pointers (aka vault show).`
|
|
30414
|
+
)
|
|
30415
|
+
);
|
|
30416
|
+
}
|
|
30417
|
+
io.stdout(`${fenced(lines.join("\n"))}
|
|
28619
30418
|
`);
|
|
28620
30419
|
}
|
|
28621
30420
|
} catch (err) {
|
|
@@ -28630,6 +30429,14 @@ async function runBackfill(deps) {
|
|
|
28630
30429
|
}
|
|
28631
30430
|
}
|
|
28632
30431
|
}
|
|
30432
|
+
function buildTranscriptScrubber() {
|
|
30433
|
+
const glue = createVaultGlue();
|
|
30434
|
+
const scope = platformRedactionScope();
|
|
30435
|
+
return (filePath) => scrubTranscriptTail(filePath, {
|
|
30436
|
+
tokenizeText: (text) => glue.tokenizeText(text, { sighting: { location: filePath, kind: "transcript" } }),
|
|
30437
|
+
scope
|
|
30438
|
+
});
|
|
30439
|
+
}
|
|
28633
30440
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
28634
30441
|
const triage = process.argv.includes("--triage");
|
|
28635
30442
|
const startedAt = Date.now();
|
|
@@ -28652,9 +30459,9 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
28652
30459
|
}
|
|
28653
30460
|
});
|
|
28654
30461
|
if (process.stdout.writableLength > 0) {
|
|
28655
|
-
await new Promise((
|
|
30462
|
+
await new Promise((resolve2) => {
|
|
28656
30463
|
process.stdout.write("", () => {
|
|
28657
|
-
|
|
30464
|
+
resolve2();
|
|
28658
30465
|
});
|
|
28659
30466
|
});
|
|
28660
30467
|
}
|