@indigoai-us/hq-cli 5.345.166 → 5.345.169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +6 -2
- package/dist/command-catalog.generated.js +7 -2
- package/dist/commands/access.js +2 -0
- package/dist/commands/agents.js +2 -2
- package/dist/commands/core-checkpoint-native.js +30 -10
- package/dist/commands/core-delegate-w1b.js +2 -1
- package/dist/commands/core-dm-bind-native.js +3 -1
- package/dist/commands/dm.d.ts +13 -2
- package/dist/commands/dm.js +42 -9
- package/dist/commands/feedback.js +11 -3
- package/dist/lib/agent-kit/mcp/tools.js +5 -6
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -1
- package/dist/lib/doctor/checks/sync-health.js +67 -2
- package/dist/lib/doctor/fix/apply.d.ts +40 -4
- package/dist/lib/doctor/fix/apply.js +183 -69
- package/dist/lib/doctor/fix/process-identity.d.ts +13 -0
- package/dist/lib/doctor/fix/process-identity.js +34 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.d.ts +201 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.js +752 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +1 -1
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +13 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +2 -0
- package/dist/lib/notify-recipient.d.ts +15 -0
- package/dist/lib/notify-recipient.js +15 -0
- package/dist/lib/secret-scan/repo-config.d.ts +8 -2
- package/dist/lib/secret-scan/repo-config.js +12 -6
- package/dist/lib/secret-scan/staged.d.ts +6 -0
- package/dist/lib/secret-scan/staged.js +21 -3
- package/dist/sentry-before-send.js +13 -0
- package/dist/utils/breadcrumb-buffer.js +15 -4
- package/dist/utils/feedback-diagnostics.js +5 -2
- package/dist/utils/feedback-logs.d.ts +7 -0
- package/dist/utils/feedback-logs.js +37 -3
- package/dist/utils/scrub-diagnostics.d.ts +4 -0
- package/dist/utils/scrub-diagnostics.js +95 -0
- package/package.json +7 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Privacy-safe outcomes for a targeted refresh-sync attempt. */
|
|
2
|
-
export declare const REFRESH_SYNC_OUTCOMES: readonly ["pull-succeeded", "pull-timeout", "pull-failed:4xx", "pull-failed:5xx", "pull-failed:network", "pull-failed:other"];
|
|
2
|
+
export declare const REFRESH_SYNC_OUTCOMES: readonly ["pull-succeeded", "pull-timeout", "pull-handed-off", "pull-preempted", "pull-timeout-capped", "pull-failed:lock-busy", "pull-failed:4xx", "pull-failed:5xx", "pull-failed:network", "pull-failed:other"];
|
|
3
3
|
export type RefreshSyncOutcome = (typeof REFRESH_SYNC_OUTCOMES)[number];
|
|
4
4
|
/** Outcomes that say nothing about the journal itself: offline, timed out, or the service failed. */
|
|
5
5
|
export declare const TRANSIENT_REFRESH_SYNC_OUTCOMES: ReadonlySet<RefreshSyncOutcome>;
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
export const REFRESH_SYNC_OUTCOMES = [
|
|
3
3
|
"pull-succeeded",
|
|
4
4
|
"pull-timeout",
|
|
5
|
+
"pull-handed-off",
|
|
6
|
+
"pull-preempted",
|
|
7
|
+
"pull-timeout-capped",
|
|
8
|
+
"pull-failed:lock-busy",
|
|
5
9
|
"pull-failed:4xx",
|
|
6
10
|
"pull-failed:5xx",
|
|
7
11
|
"pull-failed:network",
|
|
@@ -10,6 +14,10 @@ export const REFRESH_SYNC_OUTCOMES = [
|
|
|
10
14
|
/** Outcomes that say nothing about the journal itself: offline, timed out, or the service failed. */
|
|
11
15
|
export const TRANSIENT_REFRESH_SYNC_OUTCOMES = new Set([
|
|
12
16
|
"pull-timeout",
|
|
17
|
+
"pull-handed-off",
|
|
18
|
+
"pull-preempted",
|
|
19
|
+
"pull-timeout-capped",
|
|
20
|
+
"pull-failed:lock-busy",
|
|
13
21
|
"pull-failed:network",
|
|
14
22
|
"pull-failed:5xx",
|
|
15
23
|
]);
|
|
@@ -26,6 +34,11 @@ export function classifyRefreshSyncOutcome(observation) {
|
|
|
26
34
|
if (!errorCode && observation.status === 0) {
|
|
27
35
|
return "pull-succeeded";
|
|
28
36
|
}
|
|
37
|
+
// Another HQ operation (usually the sync runner's pass) held the per-root
|
|
38
|
+
// lock past --lock-timeout. Nothing is wrong with the journal itself.
|
|
39
|
+
if (/another HQ operation is already\s+running/.test(details)) {
|
|
40
|
+
return "pull-failed:lock-busy";
|
|
41
|
+
}
|
|
29
42
|
const httpStatus = /(?:HTTP(?:\/\d(?:\.\d)?)?\s+|status(?:\s+code)?\s*[=: ]\s*)([45]\d\d)\b/i.exec(details)?.[1] ?? "";
|
|
30
43
|
if (httpStatus.startsWith("4"))
|
|
31
44
|
return "pull-failed:4xx";
|
|
@@ -19,7 +19,7 @@ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTE
|
|
|
19
19
|
*/
|
|
20
20
|
export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
|
|
21
21
|
/** Stable, privacy-safe diagnostics emitted by the sync health family. */
|
|
22
|
-
export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "company-not-resolved", "legacy-personal-journal", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid", "realtime-lag-stuck", "receiver-permission-denied", "pull-access-denied", "runner-restart-loop", "device-limit-backoff", "repeated-conflicts"];
|
|
22
|
+
export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "company-not-resolved", "legacy-personal-journal", "never-synced", "invalid-last-sync", "stale-threshold", "refresh-in-progress", "refresh-lock-held", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid", "realtime-lag-stuck", "receiver-permission-denied", "pull-access-denied", "runner-restart-loop", "device-limit-backoff", "repeated-conflicts"];
|
|
23
23
|
export type SyncReasonCode = (typeof SYNC_REASON_CODES)[number];
|
|
24
24
|
/** Runtime type guard: true only for a member of the fixed vocabulary. */
|
|
25
25
|
export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
|
package/dist/lib/doctor/types.js
CHANGED
|
@@ -4,4 +4,19 @@ export declare const AGENT_UID_PATTERN: RegExp;
|
|
|
4
4
|
export declare const EMAIL_PATTERN: RegExp;
|
|
5
5
|
export declare function isRecipientUid(value: string): boolean;
|
|
6
6
|
export declare function isAgentUid(value: string): boolean;
|
|
7
|
+
/** Who a notify message is for. */
|
|
8
|
+
export type DmAudience = "human" | "agent" | "both";
|
|
9
|
+
/** What a send is addressed to, for the default audience. */
|
|
10
|
+
export type DmRecipientKind = "person" | "agent" | "channel" | "group";
|
|
11
|
+
/**
|
|
12
|
+
* Classify a single 1:1 recipient (email, prs_ or agt_ uid) for the default
|
|
13
|
+
* audience. Anything that is not an agent uid is a person.
|
|
14
|
+
*/
|
|
15
|
+
export declare function recipientKindOf(recipient: string): "person" | "agent";
|
|
16
|
+
/**
|
|
17
|
+
* Default audience by recipient: a 1:1 message to a person is for that
|
|
18
|
+
* person (`human`); a message to an agent, a channel or a group is tagged
|
|
19
|
+
* `agent` and folded away from people by default.
|
|
20
|
+
*/
|
|
21
|
+
export declare function defaultAudienceFor(kind: DmRecipientKind): DmAudience;
|
|
7
22
|
//# sourceMappingURL=notify-recipient.d.ts.map
|
|
@@ -8,4 +8,19 @@ export function isRecipientUid(value) {
|
|
|
8
8
|
export function isAgentUid(value) {
|
|
9
9
|
return AGENT_UID_PATTERN.test(value.trim());
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Classify a single 1:1 recipient (email, prs_ or agt_ uid) for the default
|
|
13
|
+
* audience. Anything that is not an agent uid is a person.
|
|
14
|
+
*/
|
|
15
|
+
export function recipientKindOf(recipient) {
|
|
16
|
+
return /^agt_/.test(recipient.trim()) ? "agent" : "person";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default audience by recipient: a 1:1 message to a person is for that
|
|
20
|
+
* person (`human`); a message to an agent, a channel or a group is tagged
|
|
21
|
+
* `agent` and folded away from people by default.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultAudienceFor(kind) {
|
|
24
|
+
return kind === "person" ? "human" : "agent";
|
|
25
|
+
}
|
|
11
26
|
//# sourceMappingURL=notify-recipient.js.map
|
|
@@ -26,13 +26,19 @@
|
|
|
26
26
|
* there, because a pattern can always be widened, alone or combined with other
|
|
27
27
|
* entries, to hide a real credential.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* An allowlist that is not acceptable is an InvalidAllowlistError. The scan
|
|
30
|
+
* then warns and runs with no repo allowlist at all (stricter, never looser),
|
|
31
|
+
* so a rejected file cannot block the commit that replaces it. Other config
|
|
32
|
+
* problems, such as an unknown --allowlist-rev, are a ScanConfigError and the
|
|
33
|
+
* scan exits 2.
|
|
31
34
|
*/
|
|
32
35
|
import { type Probe } from "./probes.js";
|
|
33
36
|
import { type CompiledAllowlist } from "./rules.js";
|
|
34
37
|
export declare class ScanConfigError extends Error {
|
|
35
38
|
}
|
|
39
|
+
/** The repo's .gitleaks.toml cannot be used; the scan ignores it (see the top of the file). */
|
|
40
|
+
export declare class InvalidAllowlistError extends ScanConfigError {
|
|
41
|
+
}
|
|
36
42
|
export declare const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
37
43
|
/**
|
|
38
44
|
* The first probe credential the allowlist would hide, judged as the detector
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
* there, because a pattern can always be widened, alone or combined with other
|
|
27
27
|
* entries, to hide a real credential.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* An allowlist that is not acceptable is an InvalidAllowlistError. The scan
|
|
30
|
+
* then warns and runs with no repo allowlist at all (stricter, never looser),
|
|
31
|
+
* so a rejected file cannot block the commit that replaces it. Other config
|
|
32
|
+
* problems, such as an unknown --allowlist-rev, are a ScanConfigError and the
|
|
33
|
+
* scan exits 2.
|
|
31
34
|
*/
|
|
32
35
|
import { execFileSync } from "node:child_process";
|
|
33
36
|
import { createHash } from "node:crypto";
|
|
@@ -38,6 +41,9 @@ import { credentialProbes } from "./probes.js";
|
|
|
38
41
|
import { compileAllowlist, compiledRules } from "./rules.js";
|
|
39
42
|
export class ScanConfigError extends Error {
|
|
40
43
|
}
|
|
44
|
+
/** The repo's .gitleaks.toml cannot be used; the scan ignores it (see the top of the file). */
|
|
45
|
+
export class InvalidAllowlistError extends ScanConfigError {
|
|
46
|
+
}
|
|
41
47
|
export const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
42
48
|
const KEYS = new Set(["description", "condition", "regexTarget", "paths", "regexes", "stopwords", "commits", "targetRules"]);
|
|
43
49
|
const MIN_PATH_PREFIX = 3;
|
|
@@ -68,7 +74,7 @@ export function suppressedProbe(al) {
|
|
|
68
74
|
return probes.find((p, i) => probeBaseline[i] && scanFragment(probeFragment(p.line), rules).length === 0);
|
|
69
75
|
}
|
|
70
76
|
function fail(file, where, msg) {
|
|
71
|
-
throw new
|
|
77
|
+
throw new InvalidAllowlistError(`${file}: ${where}: ${msg}`);
|
|
72
78
|
}
|
|
73
79
|
function stringList(file, where, key, v) {
|
|
74
80
|
if (v === undefined)
|
|
@@ -292,7 +298,7 @@ function parseToml(text, file) {
|
|
|
292
298
|
return parse(text);
|
|
293
299
|
}
|
|
294
300
|
catch (err) {
|
|
295
|
-
throw new
|
|
301
|
+
throw new InvalidAllowlistError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
function git(repoRoot, args) {
|
|
@@ -313,10 +319,10 @@ function entry(out, blobField) {
|
|
|
313
319
|
}
|
|
314
320
|
function readEntry(repoRoot, e, where) {
|
|
315
321
|
if (e.mode === "120000")
|
|
316
|
-
throw new
|
|
322
|
+
throw new InvalidAllowlistError(`${REPO_CONFIG_FILE} (${where}) is a symlink; commit a regular file`);
|
|
317
323
|
const text = git(repoRoot, ["cat-file", "blob", e.blob]);
|
|
318
324
|
if (text === null)
|
|
319
|
-
throw new
|
|
325
|
+
throw new InvalidAllowlistError(`${REPO_CONFIG_FILE} (${where}) could not be read from git`);
|
|
320
326
|
return text;
|
|
321
327
|
}
|
|
322
328
|
/**
|
|
@@ -13,6 +13,12 @@ export interface ParsedDiff {
|
|
|
13
13
|
export declare function parseUnifiedDiff(diff: string): ParsedDiff;
|
|
14
14
|
export declare function stagedDiff(cwd: string): string;
|
|
15
15
|
export declare function repoRoot(cwd: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* The repo's allowlists, or none when its .gitleaks.toml is rejected. The
|
|
18
|
+
* warning is fixed text: a validation message can quote a path or other
|
|
19
|
+
* entry, and an entry can hold a credential.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ALLOWLIST_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml allowlist was rejected; ignoring it and scanning with no repo allowlist";
|
|
16
22
|
/**
|
|
17
23
|
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
18
24
|
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { scanFragment, scanPath } from "./detect.js";
|
|
8
|
-
import { loadCommittedAllowlists, loadRepoAllowlists } from "./repo-config.js";
|
|
8
|
+
import { InvalidAllowlistError, loadCommittedAllowlists, loadRepoAllowlists } from "./repo-config.js";
|
|
9
9
|
import { compiledRules } from "./rules.js";
|
|
10
10
|
export class ScanInputError extends Error {
|
|
11
11
|
}
|
|
@@ -101,13 +101,31 @@ export function repoRoot(cwd) {
|
|
|
101
101
|
throw new ScanInputError(`could not find the repo root: ${detail || "git rev-parse failed"}`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The repo's allowlists, or none when its .gitleaks.toml is rejected. The
|
|
106
|
+
* warning is fixed text: a validation message can quote a path or other
|
|
107
|
+
* entry, and an entry can hold a credential.
|
|
108
|
+
*/
|
|
109
|
+
export const ALLOWLIST_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml allowlist was rejected; ignoring it and scanning with no repo allowlist";
|
|
110
|
+
function repoAllowlistsOrNone(load) {
|
|
111
|
+
try {
|
|
112
|
+
return load();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (!(err instanceof InvalidAllowlistError))
|
|
116
|
+
throw err;
|
|
117
|
+
process.stderr.write(`${ALLOWLIST_IGNORED_WARNING}\n`);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
104
121
|
/**
|
|
105
122
|
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
106
123
|
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
107
124
|
*/
|
|
108
125
|
export function scanStaged(cwd) {
|
|
109
126
|
const diff = stagedDiff(cwd);
|
|
110
|
-
const
|
|
127
|
+
const root = repoRoot(cwd);
|
|
128
|
+
const rules = { ...compiledRules(), repo: repoAllowlistsOrNone(() => loadRepoAllowlists(root)) };
|
|
111
129
|
const { fragments, binaryFiles } = parseUnifiedDiff(diff);
|
|
112
130
|
return [...fragments.flatMap((f) => scanFragment(f, rules)), ...binaryFiles.flatMap((f) => scanPath(f, rules))];
|
|
113
131
|
}
|
|
@@ -145,7 +163,7 @@ function matchPath(p, repo, snapshotRoot) {
|
|
|
145
163
|
}
|
|
146
164
|
export function scanFiles(paths, opts = {}) {
|
|
147
165
|
const root = opts.repo !== undefined ? repoRoot(opts.repo) : undefined;
|
|
148
|
-
const rules = root !== undefined ? { ...compiledRules(), repo: loadCommittedAllowlists(root, opts.allowlistRev) } : compiledRules();
|
|
166
|
+
const rules = root !== undefined ? { ...compiledRules(), repo: repoAllowlistsOrNone(() => loadCommittedAllowlists(root, opts.allowlistRev)) } : compiledRules();
|
|
149
167
|
const repo = root !== undefined ? real(root) : undefined;
|
|
150
168
|
const snapshotRoot = opts.snapshotRoot !== undefined ? real(opts.snapshotRoot) : undefined;
|
|
151
169
|
const out = [];
|
|
@@ -2,6 +2,13 @@ const SENSITIVE_FIELD_NAMES = new Set([
|
|
|
2
2
|
"password", "secret", "apikey", "api_key", "token",
|
|
3
3
|
// Cognito token-response fields captured as error context.
|
|
4
4
|
"idtoken", "accesstoken", "refreshtoken",
|
|
5
|
+
// Signed-URL query params, so scrubQueryString and key walks redact them.
|
|
6
|
+
"x-amz-signature", "x-amz-credential", "x-amz-security-token",
|
|
7
|
+
"x-goog-signature", "x-goog-credential", "signature", "sig",
|
|
8
|
+
// CloudFront signed-URL params, and the access-key ids paired with
|
|
9
|
+
// signatures; same names the string pattern below already covers.
|
|
10
|
+
"policy", "key-pair-id", "awsaccesskeyid", "googleaccessid",
|
|
11
|
+
"sessiontoken", "session_token",
|
|
5
12
|
]);
|
|
6
13
|
// Stored lowercased; membership tested against `name.toLowerCase()`.
|
|
7
14
|
// MUST cover BOTH Auth.js v5 (current in hq-console, pinned to
|
|
@@ -110,6 +117,10 @@ const SENSITIVE_STRING_PATTERNS = [
|
|
|
110
117
|
/\b(password|secret|api[_-]?key|(?:id|access|refresh)[_-]?token|token)\s*[:=]\s*[^\s&"'<>]+/gi,
|
|
111
118
|
/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
112
119
|
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
120
|
+
// Signed-URL params (SigV4/SigV2, GCS, CloudFront, Azure SAS): plain, at the
|
|
121
|
+
// start of a bare query string, or URL-encoded. S3 presigned URLs carry the
|
|
122
|
+
// STS session token in X-Amz-Security-Token (feedback family ec9435cd).
|
|
123
|
+
/(?:^|(?<=[?&;\s"'(]|%3F|%26))(?:X-Amz-Signature|X-Amz-Credential|X-Amz-Security-Token|X-Amz-SignedHeaders|AWSAccessKeyId|Signature|X-Goog-Signature|X-Goog-Credential|GoogleAccessId|Policy|Key-Pair-Id|sig)(?:=|%3D)[^&\s"'#<>]+/gi,
|
|
113
124
|
];
|
|
114
125
|
function scrubString(s) {
|
|
115
126
|
let out = s;
|
|
@@ -197,6 +208,8 @@ export function beforeSend(event, _hint) {
|
|
|
197
208
|
"never-synced",
|
|
198
209
|
"invalid-last-sync",
|
|
199
210
|
"stale-threshold",
|
|
211
|
+
"refresh-in-progress",
|
|
212
|
+
"refresh-lock-held",
|
|
200
213
|
"manifest-unavailable",
|
|
201
214
|
"manifest-state-unavailable",
|
|
202
215
|
"no-manifest-scopes",
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import { scrubDiagnosticsValue } from "./scrub-diagnostics.js";
|
|
1
2
|
const BUFFER_SIZE = 20;
|
|
2
3
|
const _buffer = [];
|
|
3
|
-
// Sentry beforeBreadcrumb hook: records every breadcrumb in
|
|
4
|
-
// and returns
|
|
4
|
+
// Sentry beforeBreadcrumb hook: records a scrubbed copy of every breadcrumb in
|
|
5
|
+
// a ring buffer and returns the original so Sentry still processes it (its
|
|
6
|
+
// own beforeSend scrubber runs on the way out). The buffer feeds feedback
|
|
7
|
+
// diagnostics, which beforeSend never sees, so it must never hold raw
|
|
8
|
+
// presigned URLs or tokens.
|
|
5
9
|
export function beforeBreadcrumb(breadcrumb) {
|
|
6
|
-
|
|
10
|
+
let copy;
|
|
11
|
+
try {
|
|
12
|
+
copy = scrubDiagnosticsValue(breadcrumb);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
copy = { category: breadcrumb.category, message: "[breadcrumb dropped: scrub failed]" };
|
|
16
|
+
}
|
|
17
|
+
_buffer.push(copy);
|
|
7
18
|
if (_buffer.length > BUFFER_SIZE) {
|
|
8
19
|
_buffer.shift();
|
|
9
20
|
}
|
|
10
21
|
return breadcrumb;
|
|
11
22
|
}
|
|
12
23
|
export function getRecentBreadcrumbs() {
|
|
13
|
-
return [..._buffer];
|
|
24
|
+
return scrubDiagnosticsValue([..._buffer]);
|
|
14
25
|
}
|
|
15
26
|
//# sourceMappingURL=breadcrumb-buffer.js.map
|
|
@@ -4,6 +4,7 @@ import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
|
|
|
4
4
|
import { CLI_VERSION } from "../cli-version.js";
|
|
5
5
|
import { collectVersions } from "./feedback-versions.js";
|
|
6
6
|
import { sanitizeArgv } from "./sanitize-argv.js";
|
|
7
|
+
import { scrubDiagnosticsValue } from "./scrub-diagnostics.js";
|
|
7
8
|
export { sanitizeArgv } from "./sanitize-argv.js";
|
|
8
9
|
function sanitizeRemoteUrl(url) {
|
|
9
10
|
return url.replace(/https?:\/\/[^@]+@/, "https://***@");
|
|
@@ -49,7 +50,9 @@ function collectGitContext() {
|
|
|
49
50
|
return { branch, head, dirty, remoteUrl };
|
|
50
51
|
}
|
|
51
52
|
export function collectDiagnostics() {
|
|
52
|
-
|
|
53
|
+
// Every field passes through the scrubber, not only the breadcrumbs: argv,
|
|
54
|
+
// cwd and the git remote can also carry a presigned URL or a token.
|
|
55
|
+
return scrubDiagnosticsValue({
|
|
53
56
|
cliVersion: CLI_VERSION,
|
|
54
57
|
versions: collectVersions(),
|
|
55
58
|
nodeVersion: process.version,
|
|
@@ -62,6 +65,6 @@ export function collectDiagnostics() {
|
|
|
62
65
|
cwd: process.cwd(),
|
|
63
66
|
git: collectGitContext(),
|
|
64
67
|
recentSentryBreadcrumbs: getRecentBreadcrumbs(),
|
|
65
|
-
};
|
|
68
|
+
});
|
|
66
69
|
}
|
|
67
70
|
//# sourceMappingURL=feedback-diagnostics.js.map
|
|
@@ -181,6 +181,13 @@ export interface LogCandidate {
|
|
|
181
181
|
sizeBytes: number;
|
|
182
182
|
modifiedMs: number;
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Query parameters that sign or authorize a URL: AWS SigV4 (S3 presigned
|
|
186
|
+
* URLs carry the STS session token in X-Amz-Security-Token), legacy SigV2,
|
|
187
|
+
* GCS V4, CloudFront signed URLs and Azure SAS. Exported for the feedback
|
|
188
|
+
* diagnostics scrubber.
|
|
189
|
+
*/
|
|
190
|
+
export declare const SIGNED_URL_PARAM_PATTERN: RegExp;
|
|
184
191
|
/**
|
|
185
192
|
* Replace every secret-shaped span in `text`. Returns the redacted text and a
|
|
186
193
|
* count of replacements, so triage can tell "no secrets present" from
|
|
@@ -189,6 +189,13 @@ const FEEDBACK_ROOT_LOG_FILENAMES = [
|
|
|
189
189
|
"boot-sync.log",
|
|
190
190
|
"boot-capture.log",
|
|
191
191
|
];
|
|
192
|
+
/**
|
|
193
|
+
* Query parameters that sign or authorize a URL: AWS SigV4 (S3 presigned
|
|
194
|
+
* URLs carry the STS session token in X-Amz-Security-Token), legacy SigV2,
|
|
195
|
+
* GCS V4, CloudFront signed URLs and Azure SAS. Exported for the feedback
|
|
196
|
+
* diagnostics scrubber.
|
|
197
|
+
*/
|
|
198
|
+
export const SIGNED_URL_PARAM_PATTERN = /((?:^|[?&;\s"'(]|%3F|%26)(?:X-Amz-Signature|X-Amz-Credential|X-Amz-Security-Token|X-Amz-SignedHeaders|AWSAccessKeyId|Signature|X-Goog-Signature|X-Goog-Credential|GoogleAccessId|Policy|Key-Pair-Id|sig|token|access_token|id_token|refresh_token|session_token)(?:=|%3D))[^&\s"'#<>]+/gi;
|
|
192
199
|
/**
|
|
193
200
|
* Redaction patterns, applied in order. Structured secrets (whole PEM blocks)
|
|
194
201
|
* come first so a later, narrower pattern cannot chop one into fragments that
|
|
@@ -220,20 +227,47 @@ const REDACTION_PATTERNS = [
|
|
|
220
227
|
pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g,
|
|
221
228
|
replace: () => "[redacted slack token]",
|
|
222
229
|
},
|
|
230
|
+
// HQ API keys.
|
|
231
|
+
{
|
|
232
|
+
pattern: /\bhqk_[A-Za-z0-9_-]{8,}/g,
|
|
233
|
+
replace: () => "[redacted hq key]",
|
|
234
|
+
},
|
|
223
235
|
// SigV4 presigned-URL material. Keeps the URL readable, drops what signs it.
|
|
224
236
|
{
|
|
225
|
-
|
|
237
|
+
// Matches the param at a `?`/`&`/`;` boundary, at the start of a bare
|
|
238
|
+
// query string (Sentry's `http.query` breadcrumb field), or URL-encoded
|
|
239
|
+
// (`%3F`/`%26` boundary, `%3D` separator) inside another URL.
|
|
240
|
+
pattern: SIGNED_URL_PARAM_PATTERN,
|
|
226
241
|
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
227
242
|
},
|
|
228
243
|
// `Authorization: Bearer <token>` in any casing.
|
|
244
|
+
// Signed-URL params written as header lines or quoted JSON keys
|
|
245
|
+
// (`X-Amz-Signature: v`, `"X-Amz-Credential":"v"`).
|
|
246
|
+
{
|
|
247
|
+
pattern: /("?\b(?:X-Amz-Signature|X-Amz-Credential|X-Amz-Security-Token|X-Goog-Signature|X-Goog-Credential)"?\s*:\s*"?)([^\s",;&}]+)/gi,
|
|
248
|
+
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
249
|
+
},
|
|
250
|
+
// `Authorization:` / `Proxy-Authorization:` with ANY scheme (Basic, Bearer,
|
|
251
|
+
// custom): the whole value to the end of the line or closing quote goes,
|
|
252
|
+
// however short.
|
|
253
|
+
{
|
|
254
|
+
pattern: /(\b(?:Proxy-)?Authorization"?\s*[:=]\s*"?)([^"\r\n]+)/gi,
|
|
255
|
+
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
256
|
+
},
|
|
257
|
+
// Bare scheme credentials outside a header. Basic requires a digit or a
|
|
258
|
+
// base64 symbol so prose like "Basic functionality" stays readable.
|
|
259
|
+
{
|
|
260
|
+
pattern: /\b([Bb]earer\s+)[A-Za-z0-9._~+/=-]{8,}/g,
|
|
261
|
+
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
262
|
+
},
|
|
229
263
|
{
|
|
230
|
-
pattern: /\b([Bb]
|
|
264
|
+
pattern: /\b([Bb]asic\s+)(?=[A-Za-z0-9+/=]*[0-9+/=])[A-Za-z0-9+/=]{4,}/g,
|
|
231
265
|
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
232
266
|
},
|
|
233
267
|
// Generic `key: value` / `key=value` for names that denote a secret. Runs
|
|
234
268
|
// last so the specific patterns above produce the more informative label.
|
|
235
269
|
{
|
|
236
|
-
pattern: /("?\b(?:token|secret|password|passwd|api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|authorization|credential|session[_-]?token)"?\s*[:=]\s*"?)([^\s",;&}]{6,})/gi,
|
|
270
|
+
pattern: /("?\b(?:(?:access|id|refresh)[_-]?token|token|secret|password|passwd|api[_-]?key|access[_-]?key|secret[_-]?key|auth[_-]?token|authorization|credential|session[_-]?token)"?\s*[:=]\s*"?)([^\s",;&}]{6,})/gi,
|
|
237
271
|
replace: (_m, prefix) => `${prefix}[redacted]`,
|
|
238
272
|
},
|
|
239
273
|
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isSensitiveDiagnosticsKey(key: string): boolean;
|
|
2
|
+
export declare function scrubDiagnosticsString(value: string): string;
|
|
3
|
+
export declare function scrubDiagnosticsValue<T>(value: T, seen?: WeakMap<object, unknown>): T;
|
|
4
|
+
//# sourceMappingURL=scrub-diagnostics.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { redactLogText } from "./feedback-logs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Redaction for the feedback diagnostics blob.
|
|
4
|
+
*
|
|
5
|
+
* Feedback diagnostics are uploaded to hq-pro and stored on the feedback row.
|
|
6
|
+
* Sentry's `beforeSend` scrubber never sees them: the breadcrumb ring buffer
|
|
7
|
+
* records breadcrumbs in `beforeBreadcrumb`, before any scrubbing, and
|
|
8
|
+
* `collectDiagnostics` copied that buffer verbatim. Breadcrumbs from the
|
|
9
|
+
* Node http integration carry full request URLs, so S3 presigned URLs (with
|
|
10
|
+
* the STS session token in X-Amz-Security-Token) landed in stored rows.
|
|
11
|
+
*
|
|
12
|
+
* `scrubDiagnosticsValue` deep-copies any JSON-like value, replaces values
|
|
13
|
+
* under credential-named keys, and runs every string through the same
|
|
14
|
+
* redaction patterns as the inline log tails.
|
|
15
|
+
*/
|
|
16
|
+
const REDACTED = "[redacted]";
|
|
17
|
+
// Compared after lowercasing and dropping every non-alphanumeric character,
|
|
18
|
+
// so `X-Amz-Security-Token`, `x_amz_security_token` and `xAmzSecurityToken`
|
|
19
|
+
// all normalize to the same entry.
|
|
20
|
+
const SENSITIVE_KEYS = new Set([
|
|
21
|
+
"authorization",
|
|
22
|
+
"proxyauthorization",
|
|
23
|
+
"cookie",
|
|
24
|
+
"setcookie",
|
|
25
|
+
"token",
|
|
26
|
+
"accesstoken",
|
|
27
|
+
"idtoken",
|
|
28
|
+
"refreshtoken",
|
|
29
|
+
"sessiontoken",
|
|
30
|
+
"authtoken",
|
|
31
|
+
"bearertoken",
|
|
32
|
+
"securitytoken",
|
|
33
|
+
"xamzsecuritytoken",
|
|
34
|
+
"xamzsignature",
|
|
35
|
+
"xamzcredential",
|
|
36
|
+
"xapikey",
|
|
37
|
+
"xauthtoken",
|
|
38
|
+
"xaccesstoken",
|
|
39
|
+
"xsessiontoken",
|
|
40
|
+
"apikey",
|
|
41
|
+
"secret",
|
|
42
|
+
"clientsecret",
|
|
43
|
+
"secretaccesskey",
|
|
44
|
+
"password",
|
|
45
|
+
"passwd",
|
|
46
|
+
"signature",
|
|
47
|
+
"credential",
|
|
48
|
+
"credentials",
|
|
49
|
+
// Signed-URL params that arrive as structured keys (GCS, CloudFront, Azure SAS).
|
|
50
|
+
"xgoogsignature",
|
|
51
|
+
"xgoogcredential",
|
|
52
|
+
"googleaccessid",
|
|
53
|
+
"awsaccesskeyid",
|
|
54
|
+
"sig",
|
|
55
|
+
"policy",
|
|
56
|
+
"keypairid",
|
|
57
|
+
]);
|
|
58
|
+
function normalizeKey(key) {
|
|
59
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
60
|
+
}
|
|
61
|
+
export function isSensitiveDiagnosticsKey(key) {
|
|
62
|
+
return SENSITIVE_KEYS.has(normalizeKey(key));
|
|
63
|
+
}
|
|
64
|
+
export function scrubDiagnosticsString(value) {
|
|
65
|
+
return redactLogText(value).text;
|
|
66
|
+
}
|
|
67
|
+
export function scrubDiagnosticsValue(value, seen = new WeakMap()) {
|
|
68
|
+
if (typeof value === "string")
|
|
69
|
+
return scrubDiagnosticsString(value);
|
|
70
|
+
if (value === null || typeof value !== "object")
|
|
71
|
+
return value;
|
|
72
|
+
const obj = value;
|
|
73
|
+
if (seen.has(obj))
|
|
74
|
+
return seen.get(obj);
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
const arr = [];
|
|
77
|
+
seen.set(obj, arr);
|
|
78
|
+
for (const item of value)
|
|
79
|
+
arr.push(scrubDiagnosticsValue(item, seen));
|
|
80
|
+
return arr;
|
|
81
|
+
}
|
|
82
|
+
const out = {};
|
|
83
|
+
seen.set(obj, out);
|
|
84
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
85
|
+
const safeKey = scrubDiagnosticsString(key);
|
|
86
|
+
if (isSensitiveDiagnosticsKey(key) && inner !== null && inner !== undefined && inner !== "") {
|
|
87
|
+
out[safeKey] = REDACTED;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
out[safeKey] = scrubDiagnosticsValue(inner, seen);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=scrub-diagnostics.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.169",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.169",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.169",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.169",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.169",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.169",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.169"
|
|
123
123
|
}
|
|
124
124
|
}
|