@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.
Files changed (49) hide show
  1. package/README.md +78 -1
  2. package/dist/{chunk-DIM4JRXM.js → chunk-2M57EO6U.js} +2 -2
  3. package/dist/{chunk-Y26HJUPD.js → chunk-FGDS33I2.js} +138 -12
  4. package/dist/chunk-FGDS33I2.js.map +1 -0
  5. package/dist/{chunk-MXDZHFJQ.js → chunk-JKI4OCFV.js} +4 -14
  6. package/dist/chunk-JKI4OCFV.js.map +1 -0
  7. package/dist/{chunk-7SZQN6IU.js → chunk-NB7GJE4S.js} +2 -2
  8. package/dist/chunk-NB7GJE4S.js.map +1 -0
  9. package/dist/{chunk-ZRDQ6ZKI.js → chunk-TWHCJKRS.js} +101 -481
  10. package/dist/chunk-TWHCJKRS.js.map +1 -0
  11. package/dist/{chunk-P22UQ2OJ.js → chunk-TWTWRJ25.js} +233 -9
  12. package/dist/chunk-TWTWRJ25.js.map +1 -0
  13. package/dist/cli/init.cjs +2494 -2332
  14. package/dist/cli/init.cjs.map +1 -1
  15. package/dist/cli/init.js +434 -63
  16. package/dist/cli/init.js.map +1 -1
  17. package/dist/cli/mcp-add.cjs +14 -2
  18. package/dist/cli/mcp-add.cjs.map +1 -1
  19. package/dist/cli/mcp-add.js +17 -5
  20. package/dist/cli/mcp-add.js.map +1 -1
  21. package/dist/cli/status.cjs.map +1 -1
  22. package/dist/cli/status.js +1 -3
  23. package/dist/cli/status.js.map +1 -1
  24. package/dist/cli/uninit.cjs +116 -14
  25. package/dist/cli/uninit.cjs.map +1 -1
  26. package/dist/cli/uninit.js +3 -3
  27. package/dist/cli/validate.cjs +14162 -2
  28. package/dist/cli/validate.cjs.map +1 -1
  29. package/dist/cli/validate.d.cts +7 -3
  30. package/dist/cli/validate.d.ts +7 -3
  31. package/dist/cli/validate.js +25 -2
  32. package/dist/cli/validate.js.map +1 -1
  33. package/dist/index.cjs +339 -28
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +12 -9
  36. package/dist/index.d.ts +12 -9
  37. package/dist/index.js +3 -3
  38. package/dist/{monorepo-GSL6JD3G.js → monorepo-PFVNPQ6X.js} +3 -5
  39. package/dist/node-entry.cjs +339 -28
  40. package/dist/node-entry.cjs.map +1 -1
  41. package/dist/node-entry.js +3 -3
  42. package/package.json +1 -1
  43. package/dist/chunk-7SZQN6IU.js.map +0 -1
  44. package/dist/chunk-MXDZHFJQ.js.map +0 -1
  45. package/dist/chunk-P22UQ2OJ.js.map +0 -1
  46. package/dist/chunk-Y26HJUPD.js.map +0 -1
  47. package/dist/chunk-ZRDQ6ZKI.js.map +0 -1
  48. /package/dist/{chunk-DIM4JRXM.js.map → chunk-2M57EO6U.js.map} +0 -0
  49. /package/dist/{monorepo-GSL6JD3G.js.map → monorepo-PFVNPQ6X.js.map} +0 -0
package/README.md CHANGED
@@ -4,7 +4,14 @@ Server-side debugging SDK for AI coding agents. Captures traces,
4
4
  errors, and runtime context from your Node.js application and delivers
5
5
  them to coding agents through an MCP server and live dashboard.
6
6
 
7
- > **Status: Pre-release** -- not yet published to npm.
7
+ > **Status:** Stable, published as [`@glasstrace/sdk`](https://www.npmjs.com/package/@glasstrace/sdk) on npm.
8
+ >
9
+ > ```bash
10
+ > npm install @glasstrace/sdk
11
+ > ```
12
+ >
13
+ > See [CHANGELOG.md](https://github.com/Erik-1259/glasstrace-sdk/blob/main/packages/sdk/CHANGELOG.md)
14
+ > for the release history.
8
15
 
9
16
  See the [monorepo README](../../README.md) for the full API overview,
10
17
  including the [Coexistence with Other OTel Tools](../../README.md#coexistence-with-other-otel-tools)
@@ -156,6 +163,42 @@ GLASSTRACE_SUPPRESS_ACTION_NUDGE=1
156
163
  The nudge never fires in production (detected via `NODE_ENV` or
157
164
  `VERCEL_ENV`) unless `GLASSTRACE_FORCE_ENABLE=true` is also set.
158
165
 
166
+ ## Capturing error response bodies
167
+
168
+ When debugging a 4xx or 5xx, the response body is often the most useful
169
+ signal — it carries the validation message, the tRPC error envelope, or
170
+ the upstream error code. The SDK can attach the body to the span as
171
+ `glasstrace.error.response_body`, but only under a strict three-gate
172
+ policy designed to prevent accidental leakage of customer data:
173
+
174
+ 1. **Account opt-in.** The capture is gated on the
175
+ `errorResponseBodies` flag in your account's capture configuration,
176
+ which the SDK fetches at init time. The flag defaults to `false`, so
177
+ no body is ever attached unless your account has explicitly enabled
178
+ it.
179
+ 2. **HTTP error status.** The body is only attached when the span's
180
+ HTTP status is in `[400..599]`. A successful response (2xx/3xx)
181
+ never leaks even if an upstream adapter populated the internal
182
+ attribute.
183
+ 3. **Adapter-supplied body.** The exporter does not read response
184
+ bodies itself. An adapter (e.g., a future tRPC handler wrapper) sets
185
+ the body on `glasstrace.internal.response_body`; the exporter
186
+ promotes it to the public `glasstrace.error.response_body` attribute
187
+ only when the gates above pass.
188
+
189
+ Before promotion, the body is sanitized to redact common secret
190
+ patterns — Bearer tokens, JWT-shaped tokens, Glasstrace API keys
191
+ (`gt_dev_*` / `gt_anon_*`), AWS access-key prefixes (`AKIA…` /
192
+ `ASIA…`), and generic `apikey`/`secret`/`password`/`token` key-value
193
+ pairs — and truncated to 4096 UTF-8 bytes with a `...[truncated]`
194
+ marker appended when truncation fires. Truncation respects codepoint
195
+ boundaries so multi-byte characters are never split mid-sequence.
196
+
197
+ If your account does not enable the flag, the SDK ships zero response
198
+ body data. If your account enables the flag but a span never carries
199
+ the internal attribute (no adapter set it), the public attribute is
200
+ still absent. The default is "off, twice".
201
+
159
202
  ## Browser-extension discovery
160
203
 
161
204
  `glasstrace init` writes a small static file at
@@ -306,6 +349,40 @@ edge code, but every runtime function that produces or consumes them is
306
349
  Node-only, so the practical signal is the same: reach for these from
307
350
  your build pipeline, not from a request handler.
308
351
 
352
+ #### Why is X Node-only?
353
+
354
+ Two mechanisms together produce the runtime split:
355
+
356
+ 1. **Conditional exports in `packages/sdk/package.json`** make
357
+ `@glasstrace/sdk/node` resolvable only under Node's `node` export
358
+ condition. Workerd, Vercel Edge, browsers, and any other runtime
359
+ that does not set the `node` condition fail at module resolution
360
+ rather than at evaluation. That is what keeps any given symbol off
361
+ the edge surface once it lives under `/node`.
362
+ 2. **The edge-bundle gate** (`packages/sdk/scripts/check-edge-bundle.mjs`)
363
+ then guarantees the *opposite* direction: the main edge bundle
364
+ (`dist/edge-entry.*`) is scanned for any reference to the Node
365
+ `process` global or any Node built-in specifier (`node:fs`, bare
366
+ `fs`, `fs/promises`, and so on), and the build fails if any are
367
+ found. So a symbol that reaches for `process` or a Node built-in
368
+ cannot accidentally end up on the edge side.
369
+
370
+ The gate is scope-aware about shadowing — a local binding named
371
+ `process` does not trip it — but it is deliberately not
372
+ control-flow-aware: a `process.env.X` read or a static `require("fs")`
373
+ keeps a symbol on the Node-only side even when the read is wrapped in
374
+ `typeof process !== "undefined"` or in a `try { ... } catch` guard. A
375
+ `typeof` guard means "this module reaches for `process`", and an
376
+ edge-safe module should not reach for `process` at all.
377
+
378
+ This is by design. Per the SDK-033 strict-gate policy, the contract
379
+ "this bundle passes the gate" must imply "this bundle is safe in any
380
+ edge runtime", and that implication only holds if the gate refuses
381
+ guards rather than trusting them. If you need a symbol that is currently
382
+ on the Node-only side to become edge-safe, the right move is to remove
383
+ the `process` and Node built-in reaches from the symbol's transitive
384
+ closure, not to add a runtime guard.
385
+
309
386
  ## Security
310
387
 
311
388
  The SDK transmits your API key exclusively via the `Authorization: Bearer`
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NEXT_CONFIG_NAMES
3
- } from "./chunk-7SZQN6IU.js";
3
+ } from "./chunk-NB7GJE4S.js";
4
4
 
5
5
  // src/cli/monorepo.ts
6
6
  import * as fs from "node:fs";
@@ -239,4 +239,4 @@ export {
239
239
  findNextJsApps,
240
240
  parsePnpmWorkspaceYaml
241
241
  };
242
- //# sourceMappingURL=chunk-DIM4JRXM.js.map
242
+ //# sourceMappingURL=chunk-2M57EO6U.js.map
@@ -33,16 +33,17 @@ import {
33
33
  performInit,
34
34
  recordSpansDropped,
35
35
  recordSpansExported
36
- } from "./chunk-MXDZHFJQ.js";
36
+ } from "./chunk-JKI4OCFV.js";
37
37
  import {
38
38
  isAnonymousMode,
39
39
  isProductionDisabled,
40
40
  resolveConfig
41
41
  } from "./chunk-VUZCLMIX.js";
42
42
  import {
43
+ atomicWriteFileSync,
43
44
  getOrCreateAnonKey,
44
45
  readAnonKey
45
- } from "./chunk-P22UQ2OJ.js";
46
+ } from "./chunk-TWTWRJ25.js";
46
47
  import {
47
48
  GLASSTRACE_ATTRIBUTE_NAMES,
48
49
  deriveSessionId
@@ -137,6 +138,126 @@ function classifyFetchTarget(url) {
137
138
  return "unknown";
138
139
  }
139
140
 
141
+ // src/error-response-body.ts
142
+ var ERROR_RESPONSE_BODY_MAX_BYTES = 4096;
143
+ var ERROR_RESPONSE_BODY_TRUNCATION_MARKER = "...[truncated]";
144
+ var REDACTED = "[REDACTED]";
145
+ var ERROR_STATUS_MIN = 400;
146
+ var ERROR_STATUS_MAX = 599;
147
+ function isHttpErrorStatus(status) {
148
+ let numeric;
149
+ if (typeof status === "number") {
150
+ numeric = status;
151
+ } else if (typeof status === "string" && status.length > 0) {
152
+ numeric = Number(status);
153
+ } else {
154
+ return false;
155
+ }
156
+ if (!Number.isFinite(numeric)) return false;
157
+ return numeric >= ERROR_STATUS_MIN && numeric <= ERROR_STATUS_MAX;
158
+ }
159
+ var REDACTION_PATTERNS = [
160
+ // Order matters: redact specific token shapes BEFORE the generic
161
+ // key=value catcher so a literal `Bearer eyJ…` collapses into a single
162
+ // [REDACTED] and the JWT regex does not separately match the suffix.
163
+ {
164
+ name: "bearer",
165
+ // Case-insensitive on the scheme: HTTP frameworks and proxies
166
+ // round-trip the auth scheme with inconsistent casing
167
+ // (`Bearer`, `bearer`, `BEARER`), and a real token leaks just as
168
+ // badly under any of them.
169
+ pattern: /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi
170
+ },
171
+ {
172
+ name: "jwt",
173
+ // Three base64url segments separated by dots. Real JWTs encode at
174
+ // minimum a small JSON header in the first segment, which alone is
175
+ // typically ≥10 chars after base64url; a 16-char floor avoids false
176
+ // positives on dotted text like a stack-trace frame
177
+ // (`react.dom.server`) while still catching every real JWT we have
178
+ // seen in the wild. Anchored with word boundaries on both sides so
179
+ // a 3-dot semantic version like "next@15.4.1.2" does not match.
180
+ pattern: /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
181
+ },
182
+ {
183
+ name: "glasstrace-api-key",
184
+ // gt_dev_* and gt_anon_* keys are >=24 chars of [A-Za-z0-9].
185
+ pattern: /\bgt_(?:dev|anon)_[A-Za-z0-9]{16,}\b/g
186
+ },
187
+ {
188
+ name: "aws-access-key",
189
+ // 20-char prefix-fixed identifier.
190
+ pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
191
+ },
192
+ {
193
+ name: "key-value-secret-quoted",
194
+ // Quoted-string variant: (key) [:=] "<value>". The value runs to
195
+ // the next unescaped closing quote so a multi-word secret like
196
+ // `password="my secret phrase"` is fully consumed instead of
197
+ // splitting at the first space and leaving the tail visible.
198
+ // The leading `(?<![A-Za-z0-9_])` prevents matching inside
199
+ // identifiers like `passwordless`. The trailing `"?` after the
200
+ // keyword absorbs the closing quote in JSON-style `"apikey":
201
+ // "value"` so the colon is still seen as the separator.
202
+ pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*"(?:[^"\\]|\\.)*"/gi
203
+ },
204
+ {
205
+ name: "key-value-secret-bare",
206
+ // Unquoted variant: (key) [:=] <bare-value>. The bare value
207
+ // capture stops at common JSON/text delimiters so we redact only
208
+ // the value, not surrounding structure. Listed AFTER the quoted
209
+ // variant so a quoted value's surrounding `"` are consumed by
210
+ // the first pattern and we never fall through here for a quoted
211
+ // secret.
212
+ pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*[^\s,;}\]"]+/gi
213
+ }
214
+ ];
215
+ function sanitizeErrorResponseBody(body) {
216
+ let out = body;
217
+ for (const { pattern } of REDACTION_PATTERNS) {
218
+ out = out.replace(pattern, REDACTED);
219
+ }
220
+ return out;
221
+ }
222
+ function truncateErrorResponseBody(body) {
223
+ const encoder = new TextEncoder();
224
+ const encoded = encoder.encode(body);
225
+ if (encoded.byteLength <= ERROR_RESPONSE_BODY_MAX_BYTES) {
226
+ return body;
227
+ }
228
+ let cut = ERROR_RESPONSE_BODY_MAX_BYTES;
229
+ let scan = cut - 1;
230
+ while (scan >= 0 && (encoded[scan] & 192) === 128) {
231
+ scan -= 1;
232
+ }
233
+ if (scan >= 0) {
234
+ const leading = encoded[scan];
235
+ let expected = 1;
236
+ if ((leading & 128) === 0) {
237
+ expected = 1;
238
+ } else if ((leading & 224) === 192) {
239
+ expected = 2;
240
+ } else if ((leading & 240) === 224) {
241
+ expected = 3;
242
+ } else if ((leading & 248) === 240) {
243
+ expected = 4;
244
+ }
245
+ if (scan + expected > cut) {
246
+ cut = scan;
247
+ }
248
+ }
249
+ const decoder = new TextDecoder("utf-8", { fatal: false });
250
+ const sliced = encoded.subarray(0, cut);
251
+ const decoded = decoder.decode(sliced);
252
+ return decoded + ERROR_RESPONSE_BODY_TRUNCATION_MARKER;
253
+ }
254
+ function prepareErrorResponseBody(body) {
255
+ if (body.length === 0) return null;
256
+ if (body.trim().length === 0) return null;
257
+ const sanitized = sanitizeErrorResponseBody(body);
258
+ return truncateErrorResponseBody(sanitized);
259
+ }
260
+
140
261
  // src/enriching-exporter.ts
141
262
  var ATTR = GLASSTRACE_ATTRIBUTE_NAMES;
142
263
  var API_KEY_PENDING = "pending";
@@ -365,7 +486,14 @@ var GlasstraceExporter = class {
365
486
  if (this.getConfig().errorResponseBodies) {
366
487
  const responseBody = attrs["glasstrace.internal.response_body"];
367
488
  if (typeof responseBody === "string") {
368
- extra[ATTR.ERROR_RESPONSE_BODY] = responseBody.slice(0, 500);
489
+ const enrichedStatus = extra[ATTR.HTTP_STATUS_CODE];
490
+ const effectiveStatus = typeof enrichedStatus === "number" ? enrichedStatus : statusCode;
491
+ if (isHttpErrorStatus(effectiveStatus)) {
492
+ const prepared = prepareErrorResponseBody(responseBody);
493
+ if (prepared !== null) {
494
+ extra[ATTR.ERROR_RESPONSE_BODY] = prepared;
495
+ }
496
+ }
369
497
  }
370
498
  }
371
499
  const spanAny = span;
@@ -3931,7 +4059,7 @@ function registerHeartbeatShutdownHook(config, anonKey, sdkVersion) {
3931
4059
  }
3932
4060
 
3933
4061
  // src/runtime-state.ts
3934
- import { writeFileSync, renameSync, mkdirSync } from "node:fs";
4062
+ import { mkdirSync } from "node:fs";
3935
4063
  import { join } from "node:path";
3936
4064
  var _projectRoot = null;
3937
4065
  var _sdkVersion = "unknown";
@@ -3983,12 +4111,10 @@ function writeStateNow() {
3983
4111
  };
3984
4112
  const dir = join(_projectRoot, ".glasstrace");
3985
4113
  const filePath = join(dir, "runtime-state.json");
3986
- const tmpPath = join(dir, "runtime-state.json.tmp");
3987
4114
  mkdirSync(dir, { recursive: true, mode: 448 });
3988
- writeFileSync(tmpPath, JSON.stringify(runtimeState, null, 2) + "\n", {
4115
+ atomicWriteFileSync(filePath, JSON.stringify(runtimeState, null, 2) + "\n", {
3989
4116
  mode: 384
3990
4117
  });
3991
- renameSync(tmpPath, filePath);
3992
4118
  } catch (err) {
3993
4119
  sdkLog(
3994
4120
  "warn",
@@ -4027,7 +4153,7 @@ function registerGlasstrace(options) {
4027
4153
  setCoreState(CoreState.REGISTERING);
4028
4154
  startRuntimeStateWriter({
4029
4155
  projectRoot: process.cwd(),
4030
- sdkVersion: "1.1.1"
4156
+ sdkVersion: "1.1.3"
4031
4157
  });
4032
4158
  const config = resolveConfig(options);
4033
4159
  if (config.verbose) {
@@ -4193,8 +4319,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4193
4319
  if (config.verbose) {
4194
4320
  console.info("[glasstrace] Background init firing.");
4195
4321
  }
4196
- const healthReport = collectHealthReport("1.1.1");
4197
- const initResult = await performInit(config, anonKeyForInit, "1.1.1", healthReport);
4322
+ const healthReport = collectHealthReport("1.1.3");
4323
+ const initResult = await performInit(config, anonKeyForInit, "1.1.3", healthReport);
4198
4324
  if (generation !== registrationGeneration) return;
4199
4325
  const currentState = getCoreState();
4200
4326
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4217,7 +4343,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4217
4343
  }
4218
4344
  maybeInstallConsoleCapture();
4219
4345
  if (didLastInitSucceed()) {
4220
- startHeartbeat(config, anonKeyForInit, "1.1.1", generation, (newApiKey, accountId) => {
4346
+ startHeartbeat(config, anonKeyForInit, "1.1.3", generation, (newApiKey, accountId) => {
4221
4347
  setAuthState(AuthState.CLAIMING);
4222
4348
  emitLifecycleEvent("auth:claim_started", { accountId });
4223
4349
  setResolvedApiKey(newApiKey);
@@ -4566,4 +4692,4 @@ export {
4566
4692
  withGlasstraceConfig,
4567
4693
  captureError
4568
4694
  };
4569
- //# sourceMappingURL=chunk-Y26HJUPD.js.map
4695
+ //# sourceMappingURL=chunk-FGDS33I2.js.map