@indigoai-us/hq-cli 5.5.0 → 5.5.2-sentry-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/hq-auth-refresh.js +22 -12
- package/dist/bin/hq-auth-refresh.js.map +1 -1
- package/dist/commands/_patterns.d.ts +3 -0
- package/dist/commands/_patterns.d.ts.map +1 -0
- package/dist/commands/_patterns.js +3 -0
- package/dist/commands/_patterns.js.map +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +2 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/auth.d.ts +0 -3
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +0 -3
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/cloud.d.ts +12 -2
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +112 -80
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/groups.d.ts +3 -0
- package/dist/commands/groups.d.ts.map +1 -0
- package/dist/commands/groups.js +341 -0
- package/dist/commands/groups.js.map +1 -0
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +18 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +28 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +1 -1
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +8 -3
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/onboard.d.ts +23 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +142 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/pack-install.d.ts +44 -0
- package/dist/commands/pack-install.d.ts.map +1 -0
- package/dist/commands/pack-install.js +513 -0
- package/dist/commands/pack-install.js.map +1 -0
- package/dist/commands/pkg-install.d.ts.map +1 -1
- package/dist/commands/pkg-install.js +20 -8
- package/dist/commands/pkg-install.js.map +1 -1
- package/dist/commands/pkg-list.js +4 -4
- package/dist/commands/pkg-list.js.map +1 -1
- package/dist/commands/pkg-update.js +3 -3
- package/dist/commands/pkg-update.js.map +1 -1
- package/dist/commands/secrets.d.ts +12 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +756 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +10 -2
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/team-sync.d.ts.map +1 -1
- package/dist/commands/team-sync.js +6 -6
- package/dist/commands/team-sync.js.map +1 -1
- package/dist/commands/whoami.d.ts +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +23 -7
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/sentry-before-send.d.ts +3 -0
- package/dist/sentry-before-send.d.ts.map +1 -0
- package/dist/sentry-before-send.js +216 -0
- package/dist/sentry-before-send.js.map +1 -0
- package/dist/sentry-dsn.generated.d.ts +2 -0
- package/dist/sentry-dsn.generated.d.ts.map +1 -0
- package/dist/sentry-dsn.generated.js.map +1 -0
- package/dist/sentry.d.ts +4 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +19 -0
- package/dist/sentry.js.map +1 -0
- package/dist/strategies/link.d.ts +2 -2
- package/dist/strategies/link.d.ts.map +1 -1
- package/dist/strategies/link.js.map +1 -1
- package/dist/strategies/merge.d.ts +2 -2
- package/dist/strategies/merge.d.ts.map +1 -1
- package/dist/strategies/merge.js.map +1 -1
- package/dist/types.d.ts +37 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/cognito-session.d.ts +18 -7
- package/dist/utils/cognito-session.d.ts.map +1 -1
- package/dist/utils/cognito-session.js +35 -8
- package/dist/utils/cognito-session.js.map +1 -1
- package/dist/utils/manifest.d.ts +13 -0
- package/dist/utils/manifest.d.ts.map +1 -1
- package/dist/utils/manifest.js +24 -1
- package/dist/utils/manifest.js.map +1 -1
- package/dist/utils/secrets-cache.d.ts +7 -0
- package/dist/utils/secrets-cache.d.ts.map +1 -0
- package/dist/utils/secrets-cache.js +135 -0
- package/dist/utils/secrets-cache.js.map +1 -0
- package/package.json +17 -6
- package/scripts/generate-dsn.mjs +26 -0
- package/src/bin/hq-auth-refresh.ts +23 -16
- package/src/commands/_patterns.ts +2 -0
- package/src/commands/add.ts +7 -6
- package/src/commands/auth.ts +0 -3
- package/src/commands/cloud.ts +214 -101
- package/src/commands/groups.ts +357 -0
- package/src/commands/list.ts +20 -1
- package/src/commands/login.ts +28 -9
- package/src/commands/logout.ts +8 -3
- package/src/commands/onboard.ts +193 -0
- package/src/commands/pack-install.ts +667 -0
- package/src/commands/pkg-install.ts +34 -16
- package/src/commands/pkg-list.ts +4 -4
- package/src/commands/pkg-update.ts +3 -3
- package/src/commands/secrets.ts +950 -0
- package/src/commands/sync.ts +20 -4
- package/src/commands/team-sync.ts +7 -8
- package/src/commands/whoami.ts +22 -7
- package/src/index.ts +26 -2
- package/src/sentry-before-send.test.ts +359 -0
- package/src/sentry-before-send.ts +221 -0
- package/src/sentry.test.ts +24 -0
- package/src/sentry.ts +19 -0
- package/src/strategies/link.ts +2 -2
- package/src/strategies/merge.ts +2 -2
- package/src/types.ts +54 -4
- package/src/utils/cognito-session.ts +46 -14
- package/src/utils/manifest.test.ts +148 -0
- package/src/utils/manifest.ts +22 -1
- package/src/utils/secrets-cache.ts +130 -0
- package/tsconfig.json +2 -1
- package/dist/utils/auth.d.ts +0 -27
- package/dist/utils/auth.d.ts.map +0 -1
- package/dist/utils/auth.js +0 -155
- package/dist/utils/auth.js.map +0 -1
- package/dist/utils/token-store.d.ts +0 -28
- package/dist/utils/token-store.d.ts.map +0 -1
- package/dist/utils/token-store.js +0 -68
- package/dist/utils/token-store.js.map +0 -1
- package/src/utils/auth.ts +0 -193
- package/src/utils/token-store.ts +0 -83
package/src/commands/sync.ts
CHANGED
|
@@ -23,10 +23,10 @@ import {
|
|
|
23
23
|
} from '../utils/git.js';
|
|
24
24
|
import { linkSync } from '../strategies/link.js';
|
|
25
25
|
import { mergeSync } from '../strategies/merge.js';
|
|
26
|
-
import type {
|
|
26
|
+
import type { LegacyModuleDefinition, ModuleLock, SyncResult } from '../types.js';
|
|
27
27
|
|
|
28
28
|
async function syncModule(
|
|
29
|
-
module:
|
|
29
|
+
module: LegacyModuleDefinition,
|
|
30
30
|
moduleDir: string,
|
|
31
31
|
hqRoot: string,
|
|
32
32
|
locked: boolean,
|
|
@@ -101,9 +101,25 @@ export function registerSyncCommand(program: Command): void {
|
|
|
101
101
|
const results: SyncResult[] = [];
|
|
102
102
|
const newLock: ModuleLock = { version: '1', locked: {} };
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
// strategy: package entries are wired by scripts/scan-packages.sh, not
|
|
105
|
+
// the git-repo sync flow. Partition and report separately.
|
|
106
|
+
const legacyModules = manifest.modules.filter(
|
|
107
|
+
(m): m is LegacyModuleDefinition => m.strategy !== 'package'
|
|
108
|
+
);
|
|
109
|
+
const packModules = manifest.modules.filter(
|
|
110
|
+
(m) => m.strategy === 'package'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (packModules.length > 0) {
|
|
114
|
+
console.log(
|
|
115
|
+
`Skipping ${packModules.length} package module(s) (wired via scan-packages.sh): ` +
|
|
116
|
+
packModules.map((m) => m.name).join(', ')
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Syncing ${legacyModules.length} module(s)...\n`);
|
|
105
121
|
|
|
106
|
-
for (const module of
|
|
122
|
+
for (const module of legacyModules) {
|
|
107
123
|
console.log(`[${module.name}]`);
|
|
108
124
|
const moduleDir = path.join(modulesDir, module.name);
|
|
109
125
|
|
|
@@ -17,8 +17,7 @@ import { Command } from 'commander';
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import simpleGit from 'simple-git';
|
|
19
19
|
import { findHqRoot } from '../utils/hq-root.js';
|
|
20
|
-
import {
|
|
21
|
-
import type { AuthToken } from '../utils/token-store.js';
|
|
20
|
+
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
22
21
|
|
|
23
22
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
24
23
|
|
|
@@ -57,7 +56,7 @@ interface TeamSyncResult {
|
|
|
57
56
|
|
|
58
57
|
// ─── API helpers ────────────────────────────────────────────────────────────
|
|
59
58
|
|
|
60
|
-
const API_BASE = 'https://
|
|
59
|
+
const API_BASE = 'https://example.com/api';
|
|
61
60
|
|
|
62
61
|
async function apiGet<T>(urlPath: string, token: string): Promise<T> {
|
|
63
62
|
const res = await fetch(`${API_BASE}${urlPath}`, {
|
|
@@ -255,7 +254,7 @@ function discoverTeamDirs(hqRoot: string): { dir: string; meta: TeamMetadata }[]
|
|
|
255
254
|
async function syncTeam(
|
|
256
255
|
teamDir: string,
|
|
257
256
|
meta: TeamMetadata,
|
|
258
|
-
|
|
257
|
+
accessToken: string
|
|
259
258
|
): Promise<TeamSyncResult> {
|
|
260
259
|
const result: TeamSyncResult = {
|
|
261
260
|
slug: meta.team_slug,
|
|
@@ -286,7 +285,7 @@ async function syncTeam(
|
|
|
286
285
|
try {
|
|
287
286
|
const entitlements = await fetchTeamEntitlements(
|
|
288
287
|
meta.team_id,
|
|
289
|
-
|
|
288
|
+
accessToken
|
|
290
289
|
);
|
|
291
290
|
const entitledPaths = entitlements.flatMap((e) => e.paths);
|
|
292
291
|
|
|
@@ -339,7 +338,7 @@ async function syncTeam(
|
|
|
339
338
|
try {
|
|
340
339
|
repoConfig = await fetchRepoConfig(
|
|
341
340
|
meta.team_id,
|
|
342
|
-
|
|
341
|
+
accessToken
|
|
343
342
|
);
|
|
344
343
|
} catch (err) {
|
|
345
344
|
result.message = `Failed to fetch repo credentials: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
@@ -467,9 +466,9 @@ export function registerTeamSyncCommand(program: Command): void {
|
|
|
467
466
|
}
|
|
468
467
|
|
|
469
468
|
// 2. Get valid auth token (handles refresh / re-auth prompt)
|
|
470
|
-
let authToken:
|
|
469
|
+
let authToken: string;
|
|
471
470
|
try {
|
|
472
|
-
authToken = await
|
|
471
|
+
authToken = await ensureCognitoToken();
|
|
473
472
|
} catch (err) {
|
|
474
473
|
console.error(
|
|
475
474
|
chalk.red(
|
package/src/commands/whoami.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hq whoami — displays current user or 'not logged in'
|
|
2
|
+
* hq whoami — displays current user or 'not logged in'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import {
|
|
7
|
+
import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
|
|
8
|
+
|
|
9
|
+
function peekIdToken(idToken: string): { email?: string; sub?: string } {
|
|
10
|
+
try {
|
|
11
|
+
const payload = idToken.split('.')[1];
|
|
12
|
+
if (!payload) return {};
|
|
13
|
+
const pad = payload.length % 4 === 0 ? '' : '='.repeat(4 - (payload.length % 4));
|
|
14
|
+
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/') + pad;
|
|
15
|
+
const decoded = JSON.parse(Buffer.from(normalized, 'base64').toString('utf-8'));
|
|
16
|
+
return { email: decoded.email, sub: decoded.sub };
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
export function registerWhoamiCommand(program: Command): void {
|
|
10
23
|
program
|
|
@@ -12,21 +25,23 @@ export function registerWhoamiCommand(program: Command): void {
|
|
|
12
25
|
.description('Show the currently authenticated user')
|
|
13
26
|
.action(async () => {
|
|
14
27
|
try {
|
|
15
|
-
const
|
|
28
|
+
const cached = loadCachedTokens();
|
|
16
29
|
|
|
17
|
-
if (!
|
|
30
|
+
if (!cached) {
|
|
18
31
|
console.log("Not logged in. Run 'hq login' to authenticate.");
|
|
19
32
|
return;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
if (
|
|
35
|
+
if (isExpiring(cached, 0)) {
|
|
36
|
+
const who = peekIdToken(cached.idToken).email ?? 'unknown';
|
|
23
37
|
console.log(
|
|
24
|
-
chalk.yellow(`Session expired for ${
|
|
38
|
+
chalk.yellow(`Session expired for ${who}. Run 'hq login' to re-authenticate.`)
|
|
25
39
|
);
|
|
26
40
|
return;
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
const { email, sub } = peekIdToken(cached.idToken);
|
|
44
|
+
console.log(`Logged in as ${email ?? 'unknown'}${sub ? ` (${sub})` : ''}`);
|
|
30
45
|
} catch (error) {
|
|
31
46
|
console.error(
|
|
32
47
|
chalk.red('Error:'),
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import { initSentry, Sentry } from "./sentry.js";
|
|
8
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
9
10
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
10
11
|
import { registerListCommand } from "./commands/list.js";
|
|
@@ -13,12 +14,17 @@ import { registerCloudCommands } from "./commands/cloud.js";
|
|
|
13
14
|
import { registerLoginCommand } from "./commands/login.js";
|
|
14
15
|
import { registerLogoutCommand } from "./commands/logout.js";
|
|
15
16
|
import { registerWhoamiCommand } from "./commands/whoami.js";
|
|
17
|
+
import { registerOnboardCommand } from "./commands/onboard.js";
|
|
16
18
|
import { registerPackageInstallCommand } from "./commands/pkg-install.js";
|
|
17
19
|
import { registerPackageRemoveCommand } from "./commands/pkg-remove.js";
|
|
18
20
|
import { registerPackageUpdateCommand } from "./commands/pkg-update.js";
|
|
19
21
|
import { registerPackageListCommand } from "./commands/pkg-list.js";
|
|
20
22
|
import { registerTeamSyncCommand } from "./commands/team-sync.js";
|
|
21
23
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
24
|
+
import { registerSecretsCommand } from "./commands/secrets.js";
|
|
25
|
+
import { registerGroupsCommand } from "./commands/groups.js";
|
|
26
|
+
|
|
27
|
+
initSentry();
|
|
22
28
|
|
|
23
29
|
const program = new Command();
|
|
24
30
|
|
|
@@ -63,10 +69,28 @@ registerCloudCommands(syncCmd);
|
|
|
63
69
|
// Team commands (top-level)
|
|
64
70
|
registerTeamSyncCommand(program);
|
|
65
71
|
|
|
66
|
-
// Auth commands (top-level)
|
|
72
|
+
// Auth commands (top-level — Cognito OAuth)
|
|
67
73
|
registerLoginCommand(program);
|
|
68
74
|
registerLogoutCommand(program);
|
|
69
75
|
registerWhoamiCommand(program);
|
|
70
76
|
registerAuthCommands(program);
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
// Secrets management (subcommand group — hq secrets set|get|list|delete|exec|generate-link|cache)
|
|
79
|
+
registerSecretsCommand(program);
|
|
80
|
+
|
|
81
|
+
// Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
|
|
82
|
+
registerGroupsCommand(program);
|
|
83
|
+
|
|
84
|
+
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
85
|
+
registerOnboardCommand(program);
|
|
86
|
+
|
|
87
|
+
(async () => {
|
|
88
|
+
try {
|
|
89
|
+
await program.parseAsync();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
Sentry.captureException(err);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
} finally {
|
|
94
|
+
await Sentry.flush(2000);
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/sentry-before-send.test.ts — canonical fleet Vitest suite.
|
|
2
|
+
// This file is byte-identical across every consumer repo (no per-repo
|
|
3
|
+
// line-1 variation, unlike sentry-before-send.ts). The body-identity
|
|
4
|
+
// gate each consumer runs is `diff hq-console/src/sentry-before-send.test.ts
|
|
5
|
+
// <consumer>/src/sentry-before-send.test.ts` returns empty (no tail -n +2).
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import type { ErrorEvent, EventHint } from "@sentry/nextjs";
|
|
8
|
+
import { beforeSend } from "./sentry-before-send";
|
|
9
|
+
|
|
10
|
+
const REDACTED = "[Filtered]";
|
|
11
|
+
|
|
12
|
+
function run(event: ErrorEvent): ErrorEvent {
|
|
13
|
+
const result = beforeSend(event, {} as EventHint);
|
|
14
|
+
if (!result) throw new Error("beforeSend dropped event unexpectedly");
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("scrubHeaders — SENSITIVE_HEADER_NAMES case-insensitive redaction", () => {
|
|
19
|
+
const sensitiveHeaders = [
|
|
20
|
+
"authorization",
|
|
21
|
+
"proxy-authorization",
|
|
22
|
+
"cookie",
|
|
23
|
+
"set-cookie",
|
|
24
|
+
"x-api-key",
|
|
25
|
+
"x-auth-token",
|
|
26
|
+
"x-access-token",
|
|
27
|
+
"x-session-token",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
it.each(sensitiveHeaders)("redacts %s header (and case variants)", (name) => {
|
|
31
|
+
const upper = name.toUpperCase();
|
|
32
|
+
const out = run({
|
|
33
|
+
request: {
|
|
34
|
+
headers: { [upper]: "leaky-value", "x-trace-id": "keep-me" },
|
|
35
|
+
},
|
|
36
|
+
} as unknown as ErrorEvent);
|
|
37
|
+
expect((out.request!.headers as Record<string, unknown>)[upper]).toBe(REDACTED);
|
|
38
|
+
expect((out.request!.headers as Record<string, unknown>)["x-trace-id"]).toBe("keep-me");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("redacts a raw Cookie header that lands on request.headers.cookie", () => {
|
|
42
|
+
const out = run({
|
|
43
|
+
request: {
|
|
44
|
+
headers: {
|
|
45
|
+
Cookie: "authjs.session-token=abc; other=keep",
|
|
46
|
+
"x-trace-id": "keep-me",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} as unknown as ErrorEvent);
|
|
50
|
+
expect((out.request!.headers as Record<string, unknown>).Cookie).toBe(REDACTED);
|
|
51
|
+
expect((out.request!.headers as Record<string, unknown>)["x-trace-id"]).toBe("keep-me");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("scrubCookies — SENSITIVE_COOKIE_NAMES (Auth.js v5 + v4 legacy + Clerk) + raw-string fallback", () => {
|
|
56
|
+
// v5 (Auth.js): session / callback-url use __Secure-; csrf-token uses
|
|
57
|
+
// __Host- (NOT __Secure-). OAuth-state families (pkce / state / nonce /
|
|
58
|
+
// challenge) each pair bare + __Secure-.
|
|
59
|
+
const v5Names = [
|
|
60
|
+
"authjs.session-token",
|
|
61
|
+
"__secure-authjs.session-token",
|
|
62
|
+
"authjs.csrf-token",
|
|
63
|
+
"__host-authjs.csrf-token",
|
|
64
|
+
"authjs.callback-url",
|
|
65
|
+
"__secure-authjs.callback-url",
|
|
66
|
+
"authjs.pkce.code_verifier",
|
|
67
|
+
"__secure-authjs.pkce.code_verifier",
|
|
68
|
+
"authjs.state",
|
|
69
|
+
"__secure-authjs.state",
|
|
70
|
+
"authjs.nonce",
|
|
71
|
+
"__secure-authjs.nonce",
|
|
72
|
+
"authjs.challenge",
|
|
73
|
+
"__secure-authjs.challenge",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// v4 (NextAuth legacy): same prefix discipline — csrf uses __Host-, others
|
|
77
|
+
// use __Secure-. Kept in the set so accidental downgrades / mixed-version
|
|
78
|
+
// monorepos don't silently leak.
|
|
79
|
+
const v4Names = [
|
|
80
|
+
"next-auth.session-token",
|
|
81
|
+
"__secure-next-auth.session-token",
|
|
82
|
+
"next-auth.csrf-token",
|
|
83
|
+
"__host-next-auth.csrf-token",
|
|
84
|
+
"next-auth.callback-url",
|
|
85
|
+
"__secure-next-auth.callback-url",
|
|
86
|
+
"next-auth.pkce.code_verifier",
|
|
87
|
+
"__secure-next-auth.pkce.code_verifier",
|
|
88
|
+
"next-auth.state",
|
|
89
|
+
"__secure-next-auth.state",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const clerkNames = ["__clerk_db_jwt", "__session"];
|
|
93
|
+
|
|
94
|
+
it.each([...v5Names, ...v4Names, ...clerkNames])(
|
|
95
|
+
"redacts cookie %s (case-insensitive)",
|
|
96
|
+
(name) => {
|
|
97
|
+
const out = run({
|
|
98
|
+
request: {
|
|
99
|
+
cookies: { [name.toUpperCase()]: "secret-session-value", keep_me: "ok" },
|
|
100
|
+
},
|
|
101
|
+
} as unknown as ErrorEvent);
|
|
102
|
+
const cookies = out.request!.cookies as Record<string, unknown>;
|
|
103
|
+
expect(cookies[name.toUpperCase()]).toBe(REDACTED);
|
|
104
|
+
expect(cookies.keep_me).toBe("ok");
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
it("does NOT include a __Secure-authjs.csrf-token assertion — Auth.js v5 never emits that name (uses __Host- instead)", () => {
|
|
109
|
+
// This test is a read-through spec comment: the set MUST NOT have an
|
|
110
|
+
// entry for __Secure-authjs.csrf-token. If a maintainer ever adds one
|
|
111
|
+
// mechanically-by-pattern, this test documents why it is wrong.
|
|
112
|
+
const out = run({
|
|
113
|
+
request: {
|
|
114
|
+
cookies: { "__Secure-authjs.csrf-token": "does-not-exist-in-prod" },
|
|
115
|
+
},
|
|
116
|
+
} as unknown as ErrorEvent);
|
|
117
|
+
// The value is NOT redacted because the set does not contain that name.
|
|
118
|
+
// This is deliberate — see SENSITIVE_COOKIE_NAMES comment in the module.
|
|
119
|
+
const cookies = out.request!.cookies as Record<string, unknown>;
|
|
120
|
+
expect(cookies["__Secure-authjs.csrf-token"]).toBe("does-not-exist-in-prod");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("wholesale-redacts a raw-string Cookie header", () => {
|
|
124
|
+
const out = run({
|
|
125
|
+
request: { cookies: "anything=here; other=keep" },
|
|
126
|
+
} as unknown as ErrorEvent);
|
|
127
|
+
expect(out.request!.cookies).toBe(REDACTED);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("leaves undefined cookies alone", () => {
|
|
131
|
+
const out = run({ request: { headers: {} } } as unknown as ErrorEvent);
|
|
132
|
+
expect(out.request!.cookies).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("scrubFields — recursive redaction with case-insensitivity, arrays, nested, cycles", () => {
|
|
137
|
+
it("redacts top-level sensitive field names case-insensitively", () => {
|
|
138
|
+
const out = run({
|
|
139
|
+
extra: {
|
|
140
|
+
token: "abc",
|
|
141
|
+
Password: "pw",
|
|
142
|
+
APIKEY: "k",
|
|
143
|
+
api_key: "k2",
|
|
144
|
+
IdToken: "id",
|
|
145
|
+
ACCESSTOKEN: "at",
|
|
146
|
+
RefreshToken: "rt",
|
|
147
|
+
keep: "yes",
|
|
148
|
+
},
|
|
149
|
+
} as unknown as ErrorEvent);
|
|
150
|
+
const extra = out.extra as Record<string, unknown>;
|
|
151
|
+
expect(extra.token).toBe(REDACTED);
|
|
152
|
+
expect(extra.Password).toBe(REDACTED);
|
|
153
|
+
expect(extra.APIKEY).toBe(REDACTED);
|
|
154
|
+
expect(extra.api_key).toBe(REDACTED);
|
|
155
|
+
expect(extra.IdToken).toBe(REDACTED);
|
|
156
|
+
expect(extra.ACCESSTOKEN).toBe(REDACTED);
|
|
157
|
+
expect(extra.RefreshToken).toBe(REDACTED);
|
|
158
|
+
expect(extra.keep).toBe("yes");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("recurses into nested objects and arrays", () => {
|
|
162
|
+
const out = run({
|
|
163
|
+
extra: {
|
|
164
|
+
nested: { deeper: { token: "t", safe: 1 } },
|
|
165
|
+
items: [{ password: "pw" }, { other: "ok" }],
|
|
166
|
+
},
|
|
167
|
+
} as unknown as ErrorEvent);
|
|
168
|
+
const extra = out.extra as Record<string, any>;
|
|
169
|
+
expect(extra.nested.deeper.token).toBe(REDACTED);
|
|
170
|
+
expect(extra.nested.deeper.safe).toBe(1);
|
|
171
|
+
expect(extra.items[0].password).toBe(REDACTED);
|
|
172
|
+
expect(extra.items[1].other).toBe("ok");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("handles circular references without stack overflow and scrubs fields reachable only through the cycle", () => {
|
|
176
|
+
const cyclic: Record<string, unknown> = { token: "leak" };
|
|
177
|
+
cyclic.self = cyclic;
|
|
178
|
+
const out = run({ extra: { outer: cyclic } } as unknown as ErrorEvent);
|
|
179
|
+
const outer = (out.extra as any).outer;
|
|
180
|
+
// Top-level field is scrubbed.
|
|
181
|
+
expect(outer.token).toBe(REDACTED);
|
|
182
|
+
// The Map<original, scrubbed> cycle fix means re-visits return the
|
|
183
|
+
// memoized scrubbed copy, not the un-scrubbed original — so
|
|
184
|
+
// `outer.self.token` is ALSO [Filtered], not the leaked value.
|
|
185
|
+
expect(outer.self.token).toBe(REDACTED);
|
|
186
|
+
expect(outer.self).toBe(outer);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("scalar-string defense-in-depth: redacts embedded credentials in extra.note even when the key is NOT in SENSITIVE_FIELD_NAMES", () => {
|
|
190
|
+
const out = run({
|
|
191
|
+
extra: { note: "Something broke, password=hunter2 failed" },
|
|
192
|
+
} as unknown as ErrorEvent);
|
|
193
|
+
const extra = out.extra as Record<string, unknown>;
|
|
194
|
+
expect(extra.note).toContain(`password=${REDACTED}`);
|
|
195
|
+
expect(extra.note).not.toContain("hunter2");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("scalar-string defense-in-depth: redacts a Bearer in user.bio (key NOT in SENSITIVE_FIELD_NAMES)", () => {
|
|
199
|
+
const out = run({
|
|
200
|
+
user: { id: "u1", email: "a@b.c", bio: "authorization: Bearer leak" },
|
|
201
|
+
} as unknown as ErrorEvent);
|
|
202
|
+
const user = out.user as Record<string, unknown>;
|
|
203
|
+
expect(user.id).toBe("u1");
|
|
204
|
+
expect(user.email).toBe("a@b.c");
|
|
205
|
+
expect(user.bio).toContain(`Bearer ${REDACTED}`);
|
|
206
|
+
expect(user.bio).not.toContain("leak");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("scrubString — regex passes for kv, scheme+credential, JWT triplets", () => {
|
|
211
|
+
it("redacts key=value and key: value pairs", () => {
|
|
212
|
+
const out = run({
|
|
213
|
+
message: "password=hunter2 and api-key: abc123 should be redacted",
|
|
214
|
+
} as unknown as ErrorEvent);
|
|
215
|
+
expect(out.message).toContain(`password=${REDACTED}`);
|
|
216
|
+
expect(out.message).toContain(`api-key:${REDACTED}`);
|
|
217
|
+
expect(out.message).not.toContain("hunter2");
|
|
218
|
+
expect(out.message).not.toContain("abc123");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("redacts Bearer / Basic credentials", () => {
|
|
222
|
+
const out = run({
|
|
223
|
+
message: "Got header Bearer abcDEF._~+/=- and Basic Zm9vOmJhcg==",
|
|
224
|
+
} as unknown as ErrorEvent);
|
|
225
|
+
expect(out.message).toContain(`Bearer ${REDACTED}`);
|
|
226
|
+
expect(out.message).toContain(`Basic ${REDACTED}`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("redacts non-JWT-shaped accessToken/refreshToken/id_token values in free text", () => {
|
|
230
|
+
const out = run({
|
|
231
|
+
message:
|
|
232
|
+
"refreshToken=opaqueBase64NotAJwt-xyz and accessToken: anotherOpaqueValue and id_token=short",
|
|
233
|
+
} as unknown as ErrorEvent);
|
|
234
|
+
expect(out.message).toContain(`refreshToken=${REDACTED}`);
|
|
235
|
+
expect(out.message).toContain(`accessToken:${REDACTED}`);
|
|
236
|
+
expect(out.message).toContain(`id_token=${REDACTED}`);
|
|
237
|
+
expect(out.message).not.toContain("opaqueBase64NotAJwt-xyz");
|
|
238
|
+
expect(out.message).not.toContain("anotherOpaqueValue");
|
|
239
|
+
expect(out.message).not.toContain("=short");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("redacts bare JWT triplets", () => {
|
|
243
|
+
const jwt = "eyJhbGciOi12345.eyJzdWIiOi67890.sig_abcdef1234";
|
|
244
|
+
const out = run({ message: `token was ${jwt} yeah` } as unknown as ErrorEvent);
|
|
245
|
+
expect(out.message).not.toContain(jwt);
|
|
246
|
+
expect(out.message).toContain(REDACTED);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("scrubQueryString — parse + fallback", () => {
|
|
251
|
+
it("parses well-formed query strings with a leading ? and redacts sensitive keys", () => {
|
|
252
|
+
const out = run({
|
|
253
|
+
request: {
|
|
254
|
+
query_string: "?foo=bar&token=leak&password=pw&keep=me",
|
|
255
|
+
},
|
|
256
|
+
} as unknown as ErrorEvent);
|
|
257
|
+
const qs = out.request!.query_string as string;
|
|
258
|
+
expect(qs.startsWith("?")).toBe(true);
|
|
259
|
+
expect(qs).toContain("foo=bar");
|
|
260
|
+
expect(qs).toContain(`token=${REDACTED}`);
|
|
261
|
+
expect(qs).toContain(`password=${REDACTED}`);
|
|
262
|
+
expect(qs).toContain("keep=me");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("parses no-leading-? strings as well", () => {
|
|
266
|
+
const out = run({
|
|
267
|
+
request: { query_string: "token=leak&keep=1" },
|
|
268
|
+
} as unknown as ErrorEvent);
|
|
269
|
+
const qs = out.request!.query_string as string;
|
|
270
|
+
expect(qs.startsWith("?")).toBe(false);
|
|
271
|
+
expect(qs).toContain(`token=${REDACTED}`);
|
|
272
|
+
expect(qs).toContain("keep=1");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("leaves non-string query_string alone", () => {
|
|
276
|
+
const out = run({
|
|
277
|
+
request: { query_string: undefined },
|
|
278
|
+
} as unknown as ErrorEvent);
|
|
279
|
+
expect(out.request!.query_string).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("event-level walk — exception.values, message, breadcrumbs, user, extra, contexts, request.data", () => {
|
|
284
|
+
it("scrubs exception.values[].value strings", () => {
|
|
285
|
+
const out = run({
|
|
286
|
+
exception: {
|
|
287
|
+
values: [
|
|
288
|
+
{ type: "Error", value: "password=sekret failed" },
|
|
289
|
+
{ type: "Error", value: "clean" },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
} as unknown as ErrorEvent);
|
|
293
|
+
expect(out.exception!.values![0].value).toContain(`password=${REDACTED}`);
|
|
294
|
+
expect(out.exception!.values![0].value).not.toContain("sekret");
|
|
295
|
+
expect(out.exception!.values![1].value).toBe("clean");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("scrubs event.message", () => {
|
|
299
|
+
const out = run({ message: "Bearer leaked.jwt.token" } as unknown as ErrorEvent);
|
|
300
|
+
expect(out.message).toContain(`Bearer ${REDACTED}`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("scrubs breadcrumbs.message (string) and breadcrumbs.data (structured)", () => {
|
|
304
|
+
const out = run({
|
|
305
|
+
breadcrumbs: [
|
|
306
|
+
{
|
|
307
|
+
category: "fetch",
|
|
308
|
+
message: "GET /api?token=abc",
|
|
309
|
+
data: { url: "/api", password: "leak" },
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
} as unknown as ErrorEvent);
|
|
313
|
+
const bc = out.breadcrumbs![0];
|
|
314
|
+
expect(bc.message).toContain(`token=${REDACTED}`);
|
|
315
|
+
expect((bc.data as Record<string, unknown>).password).toBe(REDACTED);
|
|
316
|
+
expect((bc.data as Record<string, unknown>).url).toBe("/api");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("scrubs event.user fields", () => {
|
|
320
|
+
const out = run({
|
|
321
|
+
user: { id: "u1", email: "a@b.c", token: "bad", IdToken: "cognito" },
|
|
322
|
+
} as unknown as ErrorEvent);
|
|
323
|
+
const user = out.user as Record<string, unknown>;
|
|
324
|
+
expect(user.id).toBe("u1");
|
|
325
|
+
expect(user.email).toBe("a@b.c");
|
|
326
|
+
expect(user.token).toBe(REDACTED);
|
|
327
|
+
expect(user.IdToken).toBe(REDACTED);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("scrubs event.extra", () => {
|
|
331
|
+
const out = run({ extra: { password: "x", safe: "y" } } as unknown as ErrorEvent);
|
|
332
|
+
expect((out.extra as Record<string, unknown>).password).toBe(REDACTED);
|
|
333
|
+
expect((out.extra as Record<string, unknown>).safe).toBe("y");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("scrubs event.contexts", () => {
|
|
337
|
+
const out = run({
|
|
338
|
+
contexts: {
|
|
339
|
+
auth: { token: "x", scheme: "bearer" },
|
|
340
|
+
runtime: { name: "node", version: "22" },
|
|
341
|
+
},
|
|
342
|
+
} as unknown as ErrorEvent);
|
|
343
|
+
const auth = (out.contexts as any).auth;
|
|
344
|
+
const runtime = (out.contexts as any).runtime;
|
|
345
|
+
expect(auth.token).toBe(REDACTED);
|
|
346
|
+
expect(auth.scheme).toBe("bearer");
|
|
347
|
+
expect(runtime.name).toBe("node");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("scrubs structured request body via request.data", () => {
|
|
351
|
+
const out = run({
|
|
352
|
+
request: { data: { username: "u", password: "pw", nested: { token: "t" } } },
|
|
353
|
+
} as unknown as ErrorEvent);
|
|
354
|
+
const data = out.request!.data as Record<string, any>;
|
|
355
|
+
expect(data.username).toBe("u");
|
|
356
|
+
expect(data.password).toBe(REDACTED);
|
|
357
|
+
expect(data.nested.token).toBe(REDACTED);
|
|
358
|
+
});
|
|
359
|
+
});
|