@glasstrace/sdk 1.1.1 → 1.1.3
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/README.md +78 -1
- package/dist/{chunk-DIM4JRXM.js → chunk-2M57EO6U.js} +2 -2
- package/dist/{chunk-Y26HJUPD.js → chunk-FGDS33I2.js} +138 -12
- package/dist/chunk-FGDS33I2.js.map +1 -0
- package/dist/{chunk-MXDZHFJQ.js → chunk-JKI4OCFV.js} +4 -14
- package/dist/chunk-JKI4OCFV.js.map +1 -0
- package/dist/{chunk-7SZQN6IU.js → chunk-NB7GJE4S.js} +2 -2
- package/dist/chunk-NB7GJE4S.js.map +1 -0
- package/dist/{chunk-ZRDQ6ZKI.js → chunk-TWHCJKRS.js} +101 -481
- package/dist/chunk-TWHCJKRS.js.map +1 -0
- package/dist/{chunk-P22UQ2OJ.js → chunk-TWTWRJ25.js} +233 -9
- package/dist/chunk-TWTWRJ25.js.map +1 -0
- package/dist/cli/init.cjs +2494 -2332
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +434 -63
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +14 -2
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +17 -5
- package/dist/cli/mcp-add.js.map +1 -1
- package/dist/cli/status.cjs.map +1 -1
- package/dist/cli/status.js +1 -3
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/uninit.cjs +116 -14
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.js +3 -3
- package/dist/cli/validate.cjs +14162 -2
- package/dist/cli/validate.cjs.map +1 -1
- package/dist/cli/validate.d.cts +7 -3
- package/dist/cli/validate.d.ts +7 -3
- package/dist/cli/validate.js +25 -2
- package/dist/cli/validate.js.map +1 -1
- package/dist/index.cjs +339 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -9
- package/dist/index.d.ts +12 -9
- package/dist/index.js +3 -3
- package/dist/{monorepo-GSL6JD3G.js → monorepo-PFVNPQ6X.js} +3 -5
- package/dist/node-entry.cjs +339 -28
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-7SZQN6IU.js.map +0 -1
- package/dist/chunk-MXDZHFJQ.js.map +0 -1
- package/dist/chunk-P22UQ2OJ.js.map +0 -1
- package/dist/chunk-Y26HJUPD.js.map +0 -1
- package/dist/chunk-ZRDQ6ZKI.js.map +0 -1
- /package/dist/{chunk-DIM4JRXM.js.map → chunk-2M57EO6U.js.map} +0 -0
- /package/dist/{monorepo-GSL6JD3G.js.map → monorepo-PFVNPQ6X.js.map} +0 -0
package/dist/cli/validate.d.cts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
interface ValidationIssue {
|
|
5
5
|
/** Stable machine-readable identifier for the issue class. */
|
|
6
|
-
code: "glasstrace-dir-without-register-import" | "sdk-import-without-glasstrace-dir" | "mcp-marker-without-configs" | "mcp-configs-without-marker";
|
|
6
|
+
code: "glasstrace-dir-without-register-import" | "sdk-import-without-glasstrace-dir" | "mcp-marker-without-configs" | "mcp-configs-without-marker" | "mcp-helper-stale-credential";
|
|
7
7
|
/** Human-readable message describing the inconsistency. */
|
|
8
8
|
message: string;
|
|
9
9
|
/** Suggested command or manual action to resolve the issue. */
|
|
@@ -39,7 +39,7 @@ declare function hasGlasstraceImport(content: string): boolean;
|
|
|
39
39
|
declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
40
40
|
/**
|
|
41
41
|
* Validates consistency between the filesystem artifacts that `sdk init`
|
|
42
|
-
* produces (DISC-1247 Scenario 4). Detects
|
|
42
|
+
* produces (DISC-1247 Scenario 4). Detects five classes of inconsistency:
|
|
43
43
|
*
|
|
44
44
|
* 1. `.glasstrace/` exists but `instrumentation.ts` does not import
|
|
45
45
|
* `registerGlasstrace` from `@glasstrace/sdk`.
|
|
@@ -47,6 +47,10 @@ declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
|
47
47
|
* from `@glasstrace/sdk`.
|
|
48
48
|
* 3. `.glasstrace/mcp-connected` marker exists but no MCP config files.
|
|
49
49
|
* 4. MCP config files exist but no `.glasstrace/mcp-connected` marker.
|
|
50
|
+
* 5. `.glasstrace/mcp-connected` marker records a credential identity
|
|
51
|
+
* that no longer matches the project's effective MCP credential
|
|
52
|
+
* (e.g. project moved from anon to account/dev-key but the managed
|
|
53
|
+
* helper config still embeds the anon bearer — DISC-1512).
|
|
50
54
|
*
|
|
51
55
|
* Each issue includes a stable `code`, a message, and a suggested fix.
|
|
52
56
|
* Exit code is non-zero whenever any issue is detected so CI pipelines
|
|
@@ -55,6 +59,6 @@ declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
|
55
59
|
* @param options - Configuration for the validator.
|
|
56
60
|
* @returns A structured result describing detected inconsistencies.
|
|
57
61
|
*/
|
|
58
|
-
declare function runValidate(options: ValidateOptions): ValidateResult
|
|
62
|
+
declare function runValidate(options: ValidateOptions): Promise<ValidateResult>;
|
|
59
63
|
|
|
60
64
|
export { type ValidateOptions, type ValidateResult, type ValidationIssue, hasGlasstraceImport, hasRegisterGlasstraceImport, runValidate };
|
package/dist/cli/validate.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
interface ValidationIssue {
|
|
5
5
|
/** Stable machine-readable identifier for the issue class. */
|
|
6
|
-
code: "glasstrace-dir-without-register-import" | "sdk-import-without-glasstrace-dir" | "mcp-marker-without-configs" | "mcp-configs-without-marker";
|
|
6
|
+
code: "glasstrace-dir-without-register-import" | "sdk-import-without-glasstrace-dir" | "mcp-marker-without-configs" | "mcp-configs-without-marker" | "mcp-helper-stale-credential";
|
|
7
7
|
/** Human-readable message describing the inconsistency. */
|
|
8
8
|
message: string;
|
|
9
9
|
/** Suggested command or manual action to resolve the issue. */
|
|
@@ -39,7 +39,7 @@ declare function hasGlasstraceImport(content: string): boolean;
|
|
|
39
39
|
declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
40
40
|
/**
|
|
41
41
|
* Validates consistency between the filesystem artifacts that `sdk init`
|
|
42
|
-
* produces (DISC-1247 Scenario 4). Detects
|
|
42
|
+
* produces (DISC-1247 Scenario 4). Detects five classes of inconsistency:
|
|
43
43
|
*
|
|
44
44
|
* 1. `.glasstrace/` exists but `instrumentation.ts` does not import
|
|
45
45
|
* `registerGlasstrace` from `@glasstrace/sdk`.
|
|
@@ -47,6 +47,10 @@ declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
|
47
47
|
* from `@glasstrace/sdk`.
|
|
48
48
|
* 3. `.glasstrace/mcp-connected` marker exists but no MCP config files.
|
|
49
49
|
* 4. MCP config files exist but no `.glasstrace/mcp-connected` marker.
|
|
50
|
+
* 5. `.glasstrace/mcp-connected` marker records a credential identity
|
|
51
|
+
* that no longer matches the project's effective MCP credential
|
|
52
|
+
* (e.g. project moved from anon to account/dev-key but the managed
|
|
53
|
+
* helper config still embeds the anon bearer — DISC-1512).
|
|
50
54
|
*
|
|
51
55
|
* Each issue includes a stable `code`, a message, and a suggested fix.
|
|
52
56
|
* Exit code is non-zero whenever any issue is detected so CI pipelines
|
|
@@ -55,6 +59,6 @@ declare function hasRegisterGlasstraceImport(content: string): boolean;
|
|
|
55
59
|
* @param options - Configuration for the validator.
|
|
56
60
|
* @returns A structured result describing detected inconsistencies.
|
|
57
61
|
*/
|
|
58
|
-
declare function runValidate(options: ValidateOptions): ValidateResult
|
|
62
|
+
declare function runValidate(options: ValidateOptions): Promise<ValidateResult>;
|
|
59
63
|
|
|
60
64
|
export { type ValidateOptions, type ValidateResult, type ValidationIssue, hasGlasstraceImport, hasRegisterGlasstraceImport, runValidate };
|
package/dist/cli/validate.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
identityFingerprint,
|
|
3
|
+
readMcpMarker,
|
|
4
|
+
resolveEffectiveMcpCredential
|
|
5
|
+
} from "../chunk-TWTWRJ25.js";
|
|
6
|
+
import "../chunk-X5MAXP5T.js";
|
|
1
7
|
import "../chunk-NSBPE2FW.js";
|
|
2
8
|
|
|
3
9
|
// src/cli/validate.ts
|
|
@@ -7,7 +13,8 @@ var MCP_CONFIG_CANDIDATES = [
|
|
|
7
13
|
".mcp.json",
|
|
8
14
|
".cursor/mcp.json",
|
|
9
15
|
".gemini/settings.json",
|
|
10
|
-
".codex/config.toml"
|
|
16
|
+
".codex/config.toml",
|
|
17
|
+
".glasstrace/mcp.json"
|
|
11
18
|
];
|
|
12
19
|
function hasGlasstraceImport(content) {
|
|
13
20
|
return /@glasstrace\/sdk/.test(content);
|
|
@@ -18,7 +25,7 @@ function hasRegisterGlasstraceImport(content) {
|
|
|
18
25
|
if (!importMatch) return false;
|
|
19
26
|
return importMatch[1].split(",").map((s) => s.trim()).includes("registerGlasstrace");
|
|
20
27
|
}
|
|
21
|
-
function runValidate(options) {
|
|
28
|
+
async function runValidate(options) {
|
|
22
29
|
const { projectRoot } = options;
|
|
23
30
|
const issues = [];
|
|
24
31
|
const glasstraceDir = path.join(projectRoot, ".glasstrace");
|
|
@@ -63,6 +70,22 @@ function runValidate(options) {
|
|
|
63
70
|
fix: "Run `npx glasstrace init` to re-register the marker, or `npx glasstrace uninit` to fully remove MCP configuration."
|
|
64
71
|
});
|
|
65
72
|
}
|
|
73
|
+
if (markerExists) {
|
|
74
|
+
try {
|
|
75
|
+
const [markerState, resolved] = await Promise.all([
|
|
76
|
+
readMcpMarker(projectRoot),
|
|
77
|
+
resolveEffectiveMcpCredential(projectRoot)
|
|
78
|
+
]);
|
|
79
|
+
if (markerState.status === "valid" && resolved.effective !== null && markerState.credentialHash !== identityFingerprint(resolved.effective.key)) {
|
|
80
|
+
issues.push({
|
|
81
|
+
code: "mcp-helper-stale-credential",
|
|
82
|
+
message: "Managed MCP configs were last refreshed with a different credential than the project is now using. MCP queries may return no traces while the dashboard sees them.",
|
|
83
|
+
fix: "Run `npx glasstrace mcp add --force` to refresh managed MCP configs with the current credential."
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
66
89
|
const summary = [];
|
|
67
90
|
if (issues.length === 0) {
|
|
68
91
|
summary.push("Glasstrace install state is consistent.");
|
package/dist/cli/validate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/validate.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n/**\n * A single artifact-state inconsistency detected by `sdk init --validate`.\n */\nexport interface ValidationIssue {\n /** Stable machine-readable identifier for the issue class. */\n code:\n | \"glasstrace-dir-without-register-import\"\n | \"sdk-import-without-glasstrace-dir\"\n | \"mcp-marker-without-configs\"\n | \"mcp-configs-without-marker\";\n /** Human-readable message describing the inconsistency. */\n message: string;\n /** Suggested command or manual action to resolve the issue. */\n fix: string;\n}\n\n/** Options for `runValidate`. */\nexport interface ValidateOptions {\n projectRoot: string;\n}\n\n/** Structured result of running the validator. */\nexport interface ValidateResult {\n /** Zero when no issues; non-zero when any issue is detected. */\n exitCode: number;\n /** Ordered lines of human-friendly summary output. */\n summary: string[];\n /** Detailed per-issue findings. */\n issues: ValidationIssue[];\n}\n\n/** MCP config files init may create. Used to detect stale state. */\nconst MCP_CONFIG_CANDIDATES = [\n \".mcp.json\",\n \".cursor/mcp.json\",\n \".gemini/settings.json\",\n \".codex/config.toml\",\n] as const;\n\n/**\n * Returns true when the given instrumentation file imports\n * `@glasstrace/sdk` (which includes the `registerGlasstrace` import\n * emitted by `sdk init`).\n *\n * @internal Exported for unit testing only.\n */\nexport function hasGlasstraceImport(content: string): boolean {\n return /@glasstrace\\/sdk/.test(content);\n}\n\n/**\n * Returns true when the file imports `registerGlasstrace` specifically\n * (as opposed to other named exports such as `withGlasstraceConfig`).\n *\n * @internal Exported for unit testing only.\n */\nexport function hasRegisterGlasstraceImport(content: string): boolean {\n // Single- or multi-specifier imports from @glasstrace/sdk that include\n // `registerGlasstrace` as a named export.\n const match = /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const importMatch = match.exec(content);\n if (!importMatch) return false;\n return importMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .includes(\"registerGlasstrace\");\n}\n\n/**\n * Validates consistency between the filesystem artifacts that `sdk init`\n * produces (DISC-1247 Scenario 4). Detects four classes of inconsistency:\n *\n * 1. `.glasstrace/` exists but `instrumentation.ts` does not import\n * `registerGlasstrace` from `@glasstrace/sdk`.\n * 2. `.glasstrace/` is missing but `instrumentation.ts` still imports\n * from `@glasstrace/sdk`.\n * 3. `.glasstrace/mcp-connected` marker exists but no MCP config files.\n * 4. MCP config files exist but no `.glasstrace/mcp-connected` marker.\n *\n * Each issue includes a stable `code`, a message, and a suggested fix.\n * Exit code is non-zero whenever any issue is detected so CI pipelines\n * can gate on `sdk init --validate`.\n *\n * @param options - Configuration for the validator.\n * @returns A structured result describing detected inconsistencies.\n */\nexport function runValidate(options: ValidateOptions): ValidateResult {\n const { projectRoot } = options;\n const issues: ValidationIssue[] = [];\n\n const glasstraceDir = path.join(projectRoot, \".glasstrace\");\n const instrumentationPath = path.join(projectRoot, \"instrumentation.ts\");\n const markerPath = path.join(glasstraceDir, \"mcp-connected\");\n\n const glasstraceDirExists = isDirectorySafe(glasstraceDir);\n const instrumentationExists = fs.existsSync(instrumentationPath);\n const instrumentationContent = instrumentationExists\n ? safeReadFile(instrumentationPath)\n : null;\n const markerExists = fs.existsSync(markerPath);\n\n const mcpConfigsPresent = MCP_CONFIG_CANDIDATES.filter((rel) =>\n fs.existsSync(path.join(projectRoot, rel)),\n );\n\n // 1. .glasstrace/ present but instrumentation missing the SDK import\n if (glasstraceDirExists) {\n if (\n instrumentationContent === null ||\n !hasRegisterGlasstraceImport(instrumentationContent)\n ) {\n issues.push({\n code: \"glasstrace-dir-without-register-import\",\n message:\n \".glasstrace/ exists but instrumentation.ts is missing the registerGlasstrace import.\",\n fix: \"Run `npx glasstrace init` to re-scaffold instrumentation.ts, or remove .glasstrace/ if the SDK is no longer in use.\",\n });\n }\n }\n\n // 2. .glasstrace/ missing but instrumentation still imports the SDK\n if (!glasstraceDirExists && instrumentationContent !== null) {\n if (hasGlasstraceImport(instrumentationContent)) {\n issues.push({\n code: \"sdk-import-without-glasstrace-dir\",\n message:\n \"instrumentation.ts imports from @glasstrace/sdk but .glasstrace/ is missing.\",\n fix: \"Run `npx glasstrace init` to recreate .glasstrace/, or `npx glasstrace uninit` to fully remove the SDK.\",\n });\n }\n }\n\n // 3. MCP marker present but no MCP config files exist\n if (markerExists && mcpConfigsPresent.length === 0) {\n issues.push({\n code: \"mcp-marker-without-configs\",\n message:\n \".glasstrace/mcp-connected marker is present but no MCP config files were found.\",\n fix: \"Run `npx glasstrace mcp add --force` to regenerate MCP configs, or delete .glasstrace/mcp-connected.\",\n });\n }\n\n // 4. MCP config files exist but no marker\n if (!markerExists && mcpConfigsPresent.length > 0) {\n issues.push({\n code: \"mcp-configs-without-marker\",\n message: `MCP config files exist (${mcpConfigsPresent.join(\", \")}) but .glasstrace/mcp-connected marker is missing.`,\n fix: \"Run `npx glasstrace init` to re-register the marker, or `npx glasstrace uninit` to fully remove MCP configuration.\",\n });\n }\n\n const summary: string[] = [];\n if (issues.length === 0) {\n summary.push(\"Glasstrace install state is consistent.\");\n } else {\n summary.push(\n `Detected ${issues.length} inconsistenc${issues.length === 1 ? \"y\" : \"ies\"} in Glasstrace install state:`,\n );\n }\n\n return {\n exitCode: issues.length > 0 ? 1 : 0,\n summary,\n issues,\n };\n}\n\n/**\n * Reads a file as UTF-8, returning `null` if the file cannot be read.\n */\nfunction safeReadFile(filePath: string): string | null {\n try {\n return fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/**\n * Returns true when the path exists and is a directory. Returns false\n * when the path is missing, is not a directory, or when `statSync`\n * throws (permission denied, TOCTOU race between existsSync and\n * statSync, etc). Validation is best-effort and must not throw — a\n * crash here would turn a reporting tool into a hard failure.\n */\nfunction isDirectorySafe(dirPath: string): boolean {\n try {\n if (!fs.existsSync(dirPath)) return false;\n return fs.statSync(dirPath).isDirectory();\n } catch {\n return false;\n }\n}\n"],"mappings":";;;AAAA,YAAY,QAAQ;AACpB,YAAY,UAAU;AAkCtB,IAAM,wBAAwB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASO,SAAS,oBAAoB,SAA0B;AAC5D,SAAO,mBAAmB,KAAK,OAAO;AACxC;AAQO,SAAS,4BAA4B,SAA0B;AAGpE,QAAM,QAAQ;AACd,QAAM,cAAc,MAAM,KAAK,OAAO;AACtC,MAAI,CAAC,YAAa,QAAO;AACzB,SAAO,YAAY,CAAC,EACjB,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,SAAS,oBAAoB;AAClC;AAoBO,SAAS,YAAY,SAA0C;AACpE,QAAM,EAAE,YAAY,IAAI;AACxB,QAAM,SAA4B,CAAC;AAEnC,QAAM,gBAAqB,UAAK,aAAa,aAAa;AAC1D,QAAM,sBAA2B,UAAK,aAAa,oBAAoB;AACvE,QAAM,aAAkB,UAAK,eAAe,eAAe;AAE3D,QAAM,sBAAsB,gBAAgB,aAAa;AACzD,QAAM,wBAA2B,cAAW,mBAAmB;AAC/D,QAAM,yBAAyB,wBAC3B,aAAa,mBAAmB,IAChC;AACJ,QAAM,eAAkB,cAAW,UAAU;AAE7C,QAAM,oBAAoB,sBAAsB;AAAA,IAAO,CAAC,QACnD,cAAgB,UAAK,aAAa,GAAG,CAAC;AAAA,EAC3C;AAGA,MAAI,qBAAqB;AACvB,QACE,2BAA2B,QAC3B,CAAC,4BAA4B,sBAAsB,GACnD;AACA,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,SACE;AAAA,QACF,KAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,CAAC,uBAAuB,2BAA2B,MAAM;AAC3D,QAAI,oBAAoB,sBAAsB,GAAG;AAC/C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,SACE;AAAA,QACF,KAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,gBAAgB,kBAAkB,WAAW,GAAG;AAClD,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,SACE;AAAA,MACF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAGA,MAAI,CAAC,gBAAgB,kBAAkB,SAAS,GAAG;AACjD,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,SAAS,2BAA2B,kBAAkB,KAAK,IAAI,CAAC;AAAA,MAChE,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,QAAM,UAAoB,CAAC;AAC3B,MAAI,OAAO,WAAW,GAAG;AACvB,YAAQ,KAAK,yCAAyC;AAAA,EACxD,OAAO;AACL,YAAQ;AAAA,MACN,YAAY,OAAO,MAAM,gBAAgB,OAAO,WAAW,IAAI,MAAM,KAAK;AAAA,IAC5E;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,OAAO,SAAS,IAAI,IAAI;AAAA,IAClC;AAAA,IACA;AAAA,EACF;AACF;AAKA,SAAS,aAAa,UAAiC;AACrD,MAAI;AACF,WAAU,gBAAa,UAAU,OAAO;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,SAAS,gBAAgB,SAA0B;AACjD,MAAI;AACF,QAAI,CAAI,cAAW,OAAO,EAAG,QAAO;AACpC,WAAU,YAAS,OAAO,EAAE,YAAY;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/validate.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n identityFingerprint,\n readMcpMarker,\n resolveEffectiveMcpCredential,\n} from \"../mcp-runtime.js\";\n\n/**\n * A single artifact-state inconsistency detected by `sdk init --validate`.\n */\nexport interface ValidationIssue {\n /** Stable machine-readable identifier for the issue class. */\n code:\n | \"glasstrace-dir-without-register-import\"\n | \"sdk-import-without-glasstrace-dir\"\n | \"mcp-marker-without-configs\"\n | \"mcp-configs-without-marker\"\n | \"mcp-helper-stale-credential\";\n /** Human-readable message describing the inconsistency. */\n message: string;\n /** Suggested command or manual action to resolve the issue. */\n fix: string;\n}\n\n/** Options for `runValidate`. */\nexport interface ValidateOptions {\n projectRoot: string;\n}\n\n/** Structured result of running the validator. */\nexport interface ValidateResult {\n /** Zero when no issues; non-zero when any issue is detected. */\n exitCode: number;\n /** Ordered lines of human-friendly summary output. */\n summary: string[];\n /** Detailed per-issue findings. */\n issues: ValidationIssue[];\n}\n\n/**\n * MCP config files init or `mcp add` may create. Used by issue classes\n * 3 and 4 to detect marker/config drift. The SDK-managed generic\n * helper at `.glasstrace/mcp.json` is included so generic-only\n * installs (e.g. CI-mode init, or workspaces whose only consumer is\n * the validation/debug query helper) validate cleanly instead of\n * being incorrectly reported as `mcp-marker-without-configs`.\n */\nconst MCP_CONFIG_CANDIDATES = [\n \".mcp.json\",\n \".cursor/mcp.json\",\n \".gemini/settings.json\",\n \".codex/config.toml\",\n \".glasstrace/mcp.json\",\n] as const;\n\n/**\n * Returns true when the given instrumentation file imports\n * `@glasstrace/sdk` (which includes the `registerGlasstrace` import\n * emitted by `sdk init`).\n *\n * @internal Exported for unit testing only.\n */\nexport function hasGlasstraceImport(content: string): boolean {\n return /@glasstrace\\/sdk/.test(content);\n}\n\n/**\n * Returns true when the file imports `registerGlasstrace` specifically\n * (as opposed to other named exports such as `withGlasstraceConfig`).\n *\n * @internal Exported for unit testing only.\n */\nexport function hasRegisterGlasstraceImport(content: string): boolean {\n // Single- or multi-specifier imports from @glasstrace/sdk that include\n // `registerGlasstrace` as a named export.\n const match = /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const importMatch = match.exec(content);\n if (!importMatch) return false;\n return importMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .includes(\"registerGlasstrace\");\n}\n\n/**\n * Validates consistency between the filesystem artifacts that `sdk init`\n * produces (DISC-1247 Scenario 4). Detects five classes of inconsistency:\n *\n * 1. `.glasstrace/` exists but `instrumentation.ts` does not import\n * `registerGlasstrace` from `@glasstrace/sdk`.\n * 2. `.glasstrace/` is missing but `instrumentation.ts` still imports\n * from `@glasstrace/sdk`.\n * 3. `.glasstrace/mcp-connected` marker exists but no MCP config files.\n * 4. MCP config files exist but no `.glasstrace/mcp-connected` marker.\n * 5. `.glasstrace/mcp-connected` marker records a credential identity\n * that no longer matches the project's effective MCP credential\n * (e.g. project moved from anon to account/dev-key but the managed\n * helper config still embeds the anon bearer — DISC-1512).\n *\n * Each issue includes a stable `code`, a message, and a suggested fix.\n * Exit code is non-zero whenever any issue is detected so CI pipelines\n * can gate on `sdk init --validate`.\n *\n * @param options - Configuration for the validator.\n * @returns A structured result describing detected inconsistencies.\n */\nexport async function runValidate(options: ValidateOptions): Promise<ValidateResult> {\n const { projectRoot } = options;\n const issues: ValidationIssue[] = [];\n\n const glasstraceDir = path.join(projectRoot, \".glasstrace\");\n const instrumentationPath = path.join(projectRoot, \"instrumentation.ts\");\n const markerPath = path.join(glasstraceDir, \"mcp-connected\");\n\n const glasstraceDirExists = isDirectorySafe(glasstraceDir);\n const instrumentationExists = fs.existsSync(instrumentationPath);\n const instrumentationContent = instrumentationExists\n ? safeReadFile(instrumentationPath)\n : null;\n const markerExists = fs.existsSync(markerPath);\n\n const mcpConfigsPresent = MCP_CONFIG_CANDIDATES.filter((rel) =>\n fs.existsSync(path.join(projectRoot, rel)),\n );\n\n // 1. .glasstrace/ present but instrumentation missing the SDK import\n if (glasstraceDirExists) {\n if (\n instrumentationContent === null ||\n !hasRegisterGlasstraceImport(instrumentationContent)\n ) {\n issues.push({\n code: \"glasstrace-dir-without-register-import\",\n message:\n \".glasstrace/ exists but instrumentation.ts is missing the registerGlasstrace import.\",\n fix: \"Run `npx glasstrace init` to re-scaffold instrumentation.ts, or remove .glasstrace/ if the SDK is no longer in use.\",\n });\n }\n }\n\n // 2. .glasstrace/ missing but instrumentation still imports the SDK\n if (!glasstraceDirExists && instrumentationContent !== null) {\n if (hasGlasstraceImport(instrumentationContent)) {\n issues.push({\n code: \"sdk-import-without-glasstrace-dir\",\n message:\n \"instrumentation.ts imports from @glasstrace/sdk but .glasstrace/ is missing.\",\n fix: \"Run `npx glasstrace init` to recreate .glasstrace/, or `npx glasstrace uninit` to fully remove the SDK.\",\n });\n }\n }\n\n // 3. MCP marker present but no MCP config files exist\n if (markerExists && mcpConfigsPresent.length === 0) {\n issues.push({\n code: \"mcp-marker-without-configs\",\n message:\n \".glasstrace/mcp-connected marker is present but no MCP config files were found.\",\n fix: \"Run `npx glasstrace mcp add --force` to regenerate MCP configs, or delete .glasstrace/mcp-connected.\",\n });\n }\n\n // 4. MCP config files exist but no marker\n if (!markerExists && mcpConfigsPresent.length > 0) {\n issues.push({\n code: \"mcp-configs-without-marker\",\n message: `MCP config files exist (${mcpConfigsPresent.join(\", \")}) but .glasstrace/mcp-connected marker is missing.`,\n fix: \"Run `npx glasstrace init` to re-register the marker, or `npx glasstrace uninit` to fully remove MCP configuration.\",\n });\n }\n\n // 5. Marker records a credential identity that no longer matches\n // the project's effective MCP credential. Catches the DISC-1512\n // case where `.glasstrace/config` is linked to an account and\n // `.env.local` carries a dev key, but managed MCP configs still\n // embed an anon bearer (so MCP queries are scoped to anon rows\n // while ingestion writes account-scoped traces).\n if (markerExists) {\n try {\n const [markerState, resolved] = await Promise.all([\n readMcpMarker(projectRoot),\n resolveEffectiveMcpCredential(projectRoot),\n ]);\n if (\n markerState.status === \"valid\" &&\n resolved.effective !== null &&\n markerState.credentialHash !== identityFingerprint(resolved.effective.key)\n ) {\n issues.push({\n code: \"mcp-helper-stale-credential\",\n message:\n \"Managed MCP configs were last refreshed with a different credential than the project is now using. MCP queries may return no traces while the dashboard sees them.\",\n fix: \"Run `npx glasstrace mcp add --force` to refresh managed MCP configs with the current credential.\",\n });\n }\n } catch {\n // Validation is best-effort and must not throw — credential\n // resolution failures here are silent and leave the issue\n // unflagged. Other classes still apply.\n }\n }\n\n const summary: string[] = [];\n if (issues.length === 0) {\n summary.push(\"Glasstrace install state is consistent.\");\n } else {\n summary.push(\n `Detected ${issues.length} inconsistenc${issues.length === 1 ? \"y\" : \"ies\"} in Glasstrace install state:`,\n );\n }\n\n return {\n exitCode: issues.length > 0 ? 1 : 0,\n summary,\n issues,\n };\n}\n\n/**\n * Reads a file as UTF-8, returning `null` if the file cannot be read.\n */\nfunction safeReadFile(filePath: string): string | null {\n try {\n return fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/**\n * Returns true when the path exists and is a directory. Returns false\n * when the path is missing, is not a directory, or when `statSync`\n * throws (permission denied, TOCTOU race between existsSync and\n * statSync, etc). Validation is best-effort and must not throw — a\n * crash here would turn a reporting tool into a hard failure.\n */\nfunction isDirectorySafe(dirPath: string): boolean {\n try {\n if (!fs.existsSync(dirPath)) return false;\n return fs.statSync(dirPath).isDirectory();\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -17398,6 +17398,208 @@ function sleep(ms, scheduler, signal) {
|
|
|
17398
17398
|
// src/mcp-runtime.ts
|
|
17399
17399
|
var import_node_crypto = require("node:crypto");
|
|
17400
17400
|
init_dist();
|
|
17401
|
+
|
|
17402
|
+
// src/atomic-write.ts
|
|
17403
|
+
function parentDir(filePath) {
|
|
17404
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
17405
|
+
const lastBackslash = filePath.lastIndexOf("\\");
|
|
17406
|
+
const lastSep = Math.max(lastSlash, lastBackslash);
|
|
17407
|
+
if (lastSep < 0) return ".";
|
|
17408
|
+
if (lastSep === 0) return filePath.slice(0, 1);
|
|
17409
|
+
return filePath.slice(0, lastSep);
|
|
17410
|
+
}
|
|
17411
|
+
var PARENT_FSYNC_SWALLOWED_CODES = /* @__PURE__ */ new Set([
|
|
17412
|
+
"EISDIR",
|
|
17413
|
+
"EINVAL",
|
|
17414
|
+
"EPERM",
|
|
17415
|
+
"ENOTSUP"
|
|
17416
|
+
]);
|
|
17417
|
+
function errnoCodeOf(err) {
|
|
17418
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
17419
|
+
const code = err.code;
|
|
17420
|
+
return typeof code === "string" ? code : void 0;
|
|
17421
|
+
}
|
|
17422
|
+
function defaultTmpPath(targetPath) {
|
|
17423
|
+
return `${targetPath}.tmp`;
|
|
17424
|
+
}
|
|
17425
|
+
var fsPromisesCache;
|
|
17426
|
+
var fsSyncCache;
|
|
17427
|
+
async function loadFsPromises() {
|
|
17428
|
+
if (fsPromisesCache !== void 0) {
|
|
17429
|
+
if (fsPromisesCache === null) {
|
|
17430
|
+
throw new Error(
|
|
17431
|
+
"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
|
|
17432
|
+
);
|
|
17433
|
+
}
|
|
17434
|
+
return fsPromisesCache;
|
|
17435
|
+
}
|
|
17436
|
+
try {
|
|
17437
|
+
fsPromisesCache = await import("node:fs/promises");
|
|
17438
|
+
return fsPromisesCache;
|
|
17439
|
+
} catch {
|
|
17440
|
+
fsPromisesCache = null;
|
|
17441
|
+
throw new Error(
|
|
17442
|
+
"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
|
|
17443
|
+
);
|
|
17444
|
+
}
|
|
17445
|
+
}
|
|
17446
|
+
function loadFsSync() {
|
|
17447
|
+
if (fsSyncCache !== void 0) {
|
|
17448
|
+
if (fsSyncCache === null) {
|
|
17449
|
+
throw new Error(
|
|
17450
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
17451
|
+
);
|
|
17452
|
+
}
|
|
17453
|
+
return fsSyncCache;
|
|
17454
|
+
}
|
|
17455
|
+
try {
|
|
17456
|
+
fsSyncCache = require("node:fs");
|
|
17457
|
+
return fsSyncCache;
|
|
17458
|
+
} catch {
|
|
17459
|
+
fsSyncCache = null;
|
|
17460
|
+
throw new Error(
|
|
17461
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
17462
|
+
);
|
|
17463
|
+
}
|
|
17464
|
+
}
|
|
17465
|
+
async function atomicWriteFile(targetPath, payload, options = {}) {
|
|
17466
|
+
return atomicWriteFileWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
|
|
17467
|
+
}
|
|
17468
|
+
async function atomicWriteFileWithTmp(targetPath, tmpPath, payload, options = {}) {
|
|
17469
|
+
const mode = options.mode ?? 384;
|
|
17470
|
+
const encoding = options.encoding ?? "utf-8";
|
|
17471
|
+
const fsp = await loadFsPromises();
|
|
17472
|
+
let handle = null;
|
|
17473
|
+
try {
|
|
17474
|
+
if (typeof payload === "string") {
|
|
17475
|
+
await fsp.writeFile(tmpPath, payload, { encoding, mode });
|
|
17476
|
+
} else {
|
|
17477
|
+
await fsp.writeFile(tmpPath, payload, { mode });
|
|
17478
|
+
}
|
|
17479
|
+
await fsp.chmod(tmpPath, mode);
|
|
17480
|
+
handle = await fsp.open(tmpPath, "r");
|
|
17481
|
+
await handle.sync();
|
|
17482
|
+
await handle.close();
|
|
17483
|
+
handle = null;
|
|
17484
|
+
await fsp.rename(tmpPath, targetPath);
|
|
17485
|
+
} catch (err) {
|
|
17486
|
+
if (handle !== null) {
|
|
17487
|
+
try {
|
|
17488
|
+
await handle.close();
|
|
17489
|
+
} catch {
|
|
17490
|
+
}
|
|
17491
|
+
}
|
|
17492
|
+
await removeTmpResidueAsync(fsp, tmpPath);
|
|
17493
|
+
throw err;
|
|
17494
|
+
}
|
|
17495
|
+
await fsyncParentDirAsync(targetPath, fsp);
|
|
17496
|
+
}
|
|
17497
|
+
async function removeTmpResidueAsync(fsp, tmpPath) {
|
|
17498
|
+
try {
|
|
17499
|
+
await fsp.unlink(tmpPath);
|
|
17500
|
+
return;
|
|
17501
|
+
} catch (err) {
|
|
17502
|
+
const code = errnoCodeOf(err);
|
|
17503
|
+
if (code !== "EISDIR" && code !== "EPERM") {
|
|
17504
|
+
return;
|
|
17505
|
+
}
|
|
17506
|
+
}
|
|
17507
|
+
try {
|
|
17508
|
+
await fsp.rmdir(tmpPath);
|
|
17509
|
+
} catch {
|
|
17510
|
+
}
|
|
17511
|
+
}
|
|
17512
|
+
async function fsyncParentDirAsync(targetPath, fsp) {
|
|
17513
|
+
const parent = parentDir(targetPath);
|
|
17514
|
+
let handle = null;
|
|
17515
|
+
try {
|
|
17516
|
+
handle = await fsp.open(parent, "r");
|
|
17517
|
+
await handle.sync();
|
|
17518
|
+
} catch (err) {
|
|
17519
|
+
const code = errnoCodeOf(err);
|
|
17520
|
+
if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
|
|
17521
|
+
return;
|
|
17522
|
+
}
|
|
17523
|
+
throw err;
|
|
17524
|
+
} finally {
|
|
17525
|
+
if (handle !== null) {
|
|
17526
|
+
try {
|
|
17527
|
+
await handle.close();
|
|
17528
|
+
} catch {
|
|
17529
|
+
}
|
|
17530
|
+
}
|
|
17531
|
+
}
|
|
17532
|
+
}
|
|
17533
|
+
function atomicWriteFileSync(targetPath, payload, options = {}) {
|
|
17534
|
+
atomicWriteFileSyncWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
|
|
17535
|
+
}
|
|
17536
|
+
function atomicWriteFileSyncWithTmp(targetPath, tmpPath, payload, options = {}) {
|
|
17537
|
+
const mode = options.mode ?? 384;
|
|
17538
|
+
const encoding = options.encoding ?? "utf-8";
|
|
17539
|
+
const fs3 = loadFsSync();
|
|
17540
|
+
let fd = null;
|
|
17541
|
+
try {
|
|
17542
|
+
if (typeof payload === "string") {
|
|
17543
|
+
fs3.writeFileSync(tmpPath, payload, { encoding, mode });
|
|
17544
|
+
} else {
|
|
17545
|
+
fs3.writeFileSync(tmpPath, payload, { mode });
|
|
17546
|
+
}
|
|
17547
|
+
fs3.chmodSync(tmpPath, mode);
|
|
17548
|
+
fd = fs3.openSync(tmpPath, "r");
|
|
17549
|
+
fs3.fsyncSync(fd);
|
|
17550
|
+
fs3.closeSync(fd);
|
|
17551
|
+
fd = null;
|
|
17552
|
+
fs3.renameSync(tmpPath, targetPath);
|
|
17553
|
+
} catch (err) {
|
|
17554
|
+
if (fd !== null) {
|
|
17555
|
+
try {
|
|
17556
|
+
fs3.closeSync(fd);
|
|
17557
|
+
} catch {
|
|
17558
|
+
}
|
|
17559
|
+
}
|
|
17560
|
+
removeTmpResidueSync(fs3, tmpPath);
|
|
17561
|
+
throw err;
|
|
17562
|
+
}
|
|
17563
|
+
fsyncParentDirSyncWithFs(targetPath, fs3);
|
|
17564
|
+
}
|
|
17565
|
+
function removeTmpResidueSync(fs3, tmpPath) {
|
|
17566
|
+
try {
|
|
17567
|
+
fs3.unlinkSync(tmpPath);
|
|
17568
|
+
return;
|
|
17569
|
+
} catch (err) {
|
|
17570
|
+
const code = errnoCodeOf(err);
|
|
17571
|
+
if (code !== "EISDIR" && code !== "EPERM") {
|
|
17572
|
+
return;
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
try {
|
|
17576
|
+
fs3.rmdirSync(tmpPath);
|
|
17577
|
+
} catch {
|
|
17578
|
+
}
|
|
17579
|
+
}
|
|
17580
|
+
function fsyncParentDirSyncWithFs(targetPath, fs3) {
|
|
17581
|
+
const parent = parentDir(targetPath);
|
|
17582
|
+
let fd = null;
|
|
17583
|
+
try {
|
|
17584
|
+
fd = fs3.openSync(parent, "r");
|
|
17585
|
+
fs3.fsyncSync(fd);
|
|
17586
|
+
} catch (err) {
|
|
17587
|
+
const code = errnoCodeOf(err);
|
|
17588
|
+
if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
|
|
17589
|
+
return;
|
|
17590
|
+
}
|
|
17591
|
+
throw err;
|
|
17592
|
+
} finally {
|
|
17593
|
+
if (fd !== null) {
|
|
17594
|
+
try {
|
|
17595
|
+
fs3.closeSync(fd);
|
|
17596
|
+
} catch {
|
|
17597
|
+
}
|
|
17598
|
+
}
|
|
17599
|
+
}
|
|
17600
|
+
}
|
|
17601
|
+
|
|
17602
|
+
// src/mcp-runtime.ts
|
|
17401
17603
|
var MCP_ENDPOINT = "https://api.glasstrace.dev/mcp";
|
|
17402
17604
|
var fsPathCache2;
|
|
17403
17605
|
async function loadFsPath2() {
|
|
@@ -17611,7 +17813,6 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
|
|
|
17611
17813
|
if (!modules) return { action: "absent" };
|
|
17612
17814
|
const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR2);
|
|
17613
17815
|
const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);
|
|
17614
|
-
const tmpPath = configPath + ".tmp";
|
|
17615
17816
|
let existing;
|
|
17616
17817
|
try {
|
|
17617
17818
|
existing = await modules.fs.readFile(configPath, "utf-8");
|
|
@@ -17628,18 +17829,12 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
|
|
|
17628
17829
|
}
|
|
17629
17830
|
const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);
|
|
17630
17831
|
try {
|
|
17631
|
-
await
|
|
17632
|
-
await modules.fs.chmod(tmpPath, 384);
|
|
17633
|
-
await modules.fs.rename(tmpPath, configPath);
|
|
17832
|
+
await atomicWriteFile(configPath, replacement, { mode: 384 });
|
|
17634
17833
|
await writeMcpMarker(projectRoot, {
|
|
17635
17834
|
credentialSource: effective.source,
|
|
17636
17835
|
credentialHash: identityFingerprint(effective.key)
|
|
17637
17836
|
});
|
|
17638
17837
|
} catch {
|
|
17639
|
-
try {
|
|
17640
|
-
await modules.fs.unlink(tmpPath);
|
|
17641
|
-
} catch {
|
|
17642
|
-
}
|
|
17643
17838
|
return { action: "preserved" };
|
|
17644
17839
|
}
|
|
17645
17840
|
emitRefreshNudge(effective.source);
|
|
@@ -17712,7 +17907,6 @@ async function saveCachedConfig(response, projectRoot) {
|
|
|
17712
17907
|
const root = projectRoot ?? process.cwd();
|
|
17713
17908
|
const dirPath = modules.path.join(root, GLASSTRACE_DIR3);
|
|
17714
17909
|
const configPath = modules.path.join(dirPath, CONFIG_FILE);
|
|
17715
|
-
const tmpPath = `${configPath}.tmp`;
|
|
17716
17910
|
try {
|
|
17717
17911
|
await modules.fs.mkdir(dirPath, { recursive: true, mode: 448 });
|
|
17718
17912
|
await modules.fs.chmod(dirPath, 448);
|
|
@@ -17720,20 +17914,10 @@ async function saveCachedConfig(response, projectRoot) {
|
|
|
17720
17914
|
response,
|
|
17721
17915
|
cachedAt: Date.now()
|
|
17722
17916
|
};
|
|
17723
|
-
await
|
|
17917
|
+
await atomicWriteFile(configPath, JSON.stringify(cached2), {
|
|
17724
17918
|
encoding: "utf-8",
|
|
17725
17919
|
mode: 384
|
|
17726
17920
|
});
|
|
17727
|
-
try {
|
|
17728
|
-
await modules.fs.chmod(tmpPath, 384);
|
|
17729
|
-
await modules.fs.rename(tmpPath, configPath);
|
|
17730
|
-
} catch (renameErr) {
|
|
17731
|
-
try {
|
|
17732
|
-
await modules.fs.unlink(tmpPath);
|
|
17733
|
-
} catch {
|
|
17734
|
-
}
|
|
17735
|
-
throw renameErr;
|
|
17736
|
-
}
|
|
17737
17921
|
await modules.fs.chmod(configPath, 384);
|
|
17738
17922
|
} catch (err) {
|
|
17739
17923
|
console.warn(
|
|
@@ -18033,6 +18217,128 @@ init_esm();
|
|
|
18033
18217
|
init_dist();
|
|
18034
18218
|
init_console_capture();
|
|
18035
18219
|
init_error_nudge();
|
|
18220
|
+
|
|
18221
|
+
// src/error-response-body.ts
|
|
18222
|
+
var ERROR_RESPONSE_BODY_MAX_BYTES = 4096;
|
|
18223
|
+
var ERROR_RESPONSE_BODY_TRUNCATION_MARKER = "...[truncated]";
|
|
18224
|
+
var REDACTED = "[REDACTED]";
|
|
18225
|
+
var ERROR_STATUS_MIN = 400;
|
|
18226
|
+
var ERROR_STATUS_MAX = 599;
|
|
18227
|
+
function isHttpErrorStatus(status) {
|
|
18228
|
+
let numeric;
|
|
18229
|
+
if (typeof status === "number") {
|
|
18230
|
+
numeric = status;
|
|
18231
|
+
} else if (typeof status === "string" && status.length > 0) {
|
|
18232
|
+
numeric = Number(status);
|
|
18233
|
+
} else {
|
|
18234
|
+
return false;
|
|
18235
|
+
}
|
|
18236
|
+
if (!Number.isFinite(numeric)) return false;
|
|
18237
|
+
return numeric >= ERROR_STATUS_MIN && numeric <= ERROR_STATUS_MAX;
|
|
18238
|
+
}
|
|
18239
|
+
var REDACTION_PATTERNS = [
|
|
18240
|
+
// Order matters: redact specific token shapes BEFORE the generic
|
|
18241
|
+
// key=value catcher so a literal `Bearer eyJ…` collapses into a single
|
|
18242
|
+
// [REDACTED] and the JWT regex does not separately match the suffix.
|
|
18243
|
+
{
|
|
18244
|
+
name: "bearer",
|
|
18245
|
+
// Case-insensitive on the scheme: HTTP frameworks and proxies
|
|
18246
|
+
// round-trip the auth scheme with inconsistent casing
|
|
18247
|
+
// (`Bearer`, `bearer`, `BEARER`), and a real token leaks just as
|
|
18248
|
+
// badly under any of them.
|
|
18249
|
+
pattern: /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi
|
|
18250
|
+
},
|
|
18251
|
+
{
|
|
18252
|
+
name: "jwt",
|
|
18253
|
+
// Three base64url segments separated by dots. Real JWTs encode at
|
|
18254
|
+
// minimum a small JSON header in the first segment, which alone is
|
|
18255
|
+
// typically ≥10 chars after base64url; a 16-char floor avoids false
|
|
18256
|
+
// positives on dotted text like a stack-trace frame
|
|
18257
|
+
// (`react.dom.server`) while still catching every real JWT we have
|
|
18258
|
+
// seen in the wild. Anchored with word boundaries on both sides so
|
|
18259
|
+
// a 3-dot semantic version like "next@15.4.1.2" does not match.
|
|
18260
|
+
pattern: /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
|
|
18261
|
+
},
|
|
18262
|
+
{
|
|
18263
|
+
name: "glasstrace-api-key",
|
|
18264
|
+
// gt_dev_* and gt_anon_* keys are >=24 chars of [A-Za-z0-9].
|
|
18265
|
+
pattern: /\bgt_(?:dev|anon)_[A-Za-z0-9]{16,}\b/g
|
|
18266
|
+
},
|
|
18267
|
+
{
|
|
18268
|
+
name: "aws-access-key",
|
|
18269
|
+
// 20-char prefix-fixed identifier.
|
|
18270
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
|
|
18271
|
+
},
|
|
18272
|
+
{
|
|
18273
|
+
name: "key-value-secret-quoted",
|
|
18274
|
+
// Quoted-string variant: (key) [:=] "<value>". The value runs to
|
|
18275
|
+
// the next unescaped closing quote so a multi-word secret like
|
|
18276
|
+
// `password="my secret phrase"` is fully consumed instead of
|
|
18277
|
+
// splitting at the first space and leaving the tail visible.
|
|
18278
|
+
// The leading `(?<![A-Za-z0-9_])` prevents matching inside
|
|
18279
|
+
// identifiers like `passwordless`. The trailing `"?` after the
|
|
18280
|
+
// keyword absorbs the closing quote in JSON-style `"apikey":
|
|
18281
|
+
// "value"` so the colon is still seen as the separator.
|
|
18282
|
+
pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*"(?:[^"\\]|\\.)*"/gi
|
|
18283
|
+
},
|
|
18284
|
+
{
|
|
18285
|
+
name: "key-value-secret-bare",
|
|
18286
|
+
// Unquoted variant: (key) [:=] <bare-value>. The bare value
|
|
18287
|
+
// capture stops at common JSON/text delimiters so we redact only
|
|
18288
|
+
// the value, not surrounding structure. Listed AFTER the quoted
|
|
18289
|
+
// variant so a quoted value's surrounding `"` are consumed by
|
|
18290
|
+
// the first pattern and we never fall through here for a quoted
|
|
18291
|
+
// secret.
|
|
18292
|
+
pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*[^\s,;}\]"]+/gi
|
|
18293
|
+
}
|
|
18294
|
+
];
|
|
18295
|
+
function sanitizeErrorResponseBody(body) {
|
|
18296
|
+
let out = body;
|
|
18297
|
+
for (const { pattern } of REDACTION_PATTERNS) {
|
|
18298
|
+
out = out.replace(pattern, REDACTED);
|
|
18299
|
+
}
|
|
18300
|
+
return out;
|
|
18301
|
+
}
|
|
18302
|
+
function truncateErrorResponseBody(body) {
|
|
18303
|
+
const encoder = new TextEncoder();
|
|
18304
|
+
const encoded = encoder.encode(body);
|
|
18305
|
+
if (encoded.byteLength <= ERROR_RESPONSE_BODY_MAX_BYTES) {
|
|
18306
|
+
return body;
|
|
18307
|
+
}
|
|
18308
|
+
let cut = ERROR_RESPONSE_BODY_MAX_BYTES;
|
|
18309
|
+
let scan = cut - 1;
|
|
18310
|
+
while (scan >= 0 && (encoded[scan] & 192) === 128) {
|
|
18311
|
+
scan -= 1;
|
|
18312
|
+
}
|
|
18313
|
+
if (scan >= 0) {
|
|
18314
|
+
const leading = encoded[scan];
|
|
18315
|
+
let expected = 1;
|
|
18316
|
+
if ((leading & 128) === 0) {
|
|
18317
|
+
expected = 1;
|
|
18318
|
+
} else if ((leading & 224) === 192) {
|
|
18319
|
+
expected = 2;
|
|
18320
|
+
} else if ((leading & 240) === 224) {
|
|
18321
|
+
expected = 3;
|
|
18322
|
+
} else if ((leading & 248) === 240) {
|
|
18323
|
+
expected = 4;
|
|
18324
|
+
}
|
|
18325
|
+
if (scan + expected > cut) {
|
|
18326
|
+
cut = scan;
|
|
18327
|
+
}
|
|
18328
|
+
}
|
|
18329
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
18330
|
+
const sliced = encoded.subarray(0, cut);
|
|
18331
|
+
const decoded = decoder.decode(sliced);
|
|
18332
|
+
return decoded + ERROR_RESPONSE_BODY_TRUNCATION_MARKER;
|
|
18333
|
+
}
|
|
18334
|
+
function prepareErrorResponseBody(body) {
|
|
18335
|
+
if (body.length === 0) return null;
|
|
18336
|
+
if (body.trim().length === 0) return null;
|
|
18337
|
+
const sanitized = sanitizeErrorResponseBody(body);
|
|
18338
|
+
return truncateErrorResponseBody(sanitized);
|
|
18339
|
+
}
|
|
18340
|
+
|
|
18341
|
+
// src/enriching-exporter.ts
|
|
18036
18342
|
var ATTR = GLASSTRACE_ATTRIBUTE_NAMES;
|
|
18037
18343
|
var API_KEY_PENDING = "pending";
|
|
18038
18344
|
var MAX_PENDING_SPANS = 1024;
|
|
@@ -18260,7 +18566,14 @@ var GlasstraceExporter = class {
|
|
|
18260
18566
|
if (this.getConfig().errorResponseBodies) {
|
|
18261
18567
|
const responseBody = attrs["glasstrace.internal.response_body"];
|
|
18262
18568
|
if (typeof responseBody === "string") {
|
|
18263
|
-
extra[ATTR.
|
|
18569
|
+
const enrichedStatus = extra[ATTR.HTTP_STATUS_CODE];
|
|
18570
|
+
const effectiveStatus = typeof enrichedStatus === "number" ? enrichedStatus : statusCode;
|
|
18571
|
+
if (isHttpErrorStatus(effectiveStatus)) {
|
|
18572
|
+
const prepared = prepareErrorResponseBody(responseBody);
|
|
18573
|
+
if (prepared !== null) {
|
|
18574
|
+
extra[ATTR.ERROR_RESPONSE_BODY] = prepared;
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18264
18577
|
}
|
|
18265
18578
|
}
|
|
18266
18579
|
const spanAny = span;
|
|
@@ -22082,12 +22395,10 @@ function writeStateNow() {
|
|
|
22082
22395
|
};
|
|
22083
22396
|
const dir = (0, import_node_path.join)(_projectRoot, ".glasstrace");
|
|
22084
22397
|
const filePath = (0, import_node_path.join)(dir, "runtime-state.json");
|
|
22085
|
-
const tmpPath = (0, import_node_path.join)(dir, "runtime-state.json.tmp");
|
|
22086
22398
|
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
22087
|
-
(
|
|
22399
|
+
atomicWriteFileSync(filePath, JSON.stringify(runtimeState, null, 2) + "\n", {
|
|
22088
22400
|
mode: 384
|
|
22089
22401
|
});
|
|
22090
|
-
(0, import_node_fs.renameSync)(tmpPath, filePath);
|
|
22091
22402
|
} catch (err) {
|
|
22092
22403
|
sdkLog(
|
|
22093
22404
|
"warn",
|
|
@@ -22126,7 +22437,7 @@ function registerGlasstrace(options) {
|
|
|
22126
22437
|
setCoreState(CoreState.REGISTERING);
|
|
22127
22438
|
startRuntimeStateWriter({
|
|
22128
22439
|
projectRoot: process.cwd(),
|
|
22129
|
-
sdkVersion: "1.1.
|
|
22440
|
+
sdkVersion: "1.1.3"
|
|
22130
22441
|
});
|
|
22131
22442
|
const config2 = resolveConfig(options);
|
|
22132
22443
|
if (config2.verbose) {
|
|
@@ -22292,8 +22603,8 @@ async function backgroundInit(config2, anonKeyForInit, generation) {
|
|
|
22292
22603
|
if (config2.verbose) {
|
|
22293
22604
|
console.info("[glasstrace] Background init firing.");
|
|
22294
22605
|
}
|
|
22295
|
-
const healthReport = collectHealthReport("1.1.
|
|
22296
|
-
const initResult = await performInit(config2, anonKeyForInit, "1.1.
|
|
22606
|
+
const healthReport = collectHealthReport("1.1.3");
|
|
22607
|
+
const initResult = await performInit(config2, anonKeyForInit, "1.1.3", healthReport);
|
|
22297
22608
|
if (generation !== registrationGeneration) return;
|
|
22298
22609
|
const currentState = getCoreState();
|
|
22299
22610
|
if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
|
|
@@ -22316,7 +22627,7 @@ async function backgroundInit(config2, anonKeyForInit, generation) {
|
|
|
22316
22627
|
}
|
|
22317
22628
|
maybeInstallConsoleCapture();
|
|
22318
22629
|
if (didLastInitSucceed()) {
|
|
22319
|
-
startHeartbeat(config2, anonKeyForInit, "1.1.
|
|
22630
|
+
startHeartbeat(config2, anonKeyForInit, "1.1.3", generation, (newApiKey, accountId) => {
|
|
22320
22631
|
setAuthState(AuthState.CLAIMING);
|
|
22321
22632
|
emitLifecycleEvent("auth:claim_started", { accountId });
|
|
22322
22633
|
setResolvedApiKey(newApiKey);
|