@glasstrace/sdk 1.1.2 → 1.2.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/index.d.cts CHANGED
@@ -88,15 +88,18 @@ declare function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey>;
88
88
  */
89
89
  declare function loadCachedConfig(projectRoot?: string): SdkInitResponse | null;
90
90
  /**
91
- * Persists the init response to `.glasstrace/config` using atomic
92
- * write-temp + rename semantics. Silently skipped when `node:fs` is
93
- * unavailable (non-Node environments). On I/O failure, logs a warning.
94
- *
95
- * Atomicity: the payload is written to `.glasstrace/config.tmp` and then
96
- * renamed into place. `rename` is atomic on POSIX filesystems, so readers
97
- * either see the previous valid config or the new valid config — never a
98
- * truncated or partially-written file (DISC-1247 Scenario 5). If the
99
- * rename fails, the temp file is cleaned up on a best-effort basis.
91
+ * Persists the init response to `.glasstrace/config` using the SDK 2.0
92
+ * atomic-write protocol (`tmp + fsync(tmp) + rename + fsync(parent)`).
93
+ * Silently skipped when `node:fs` is unavailable (non-Node environments).
94
+ * On I/O failure, logs a warning.
95
+ *
96
+ * Atomicity: the payload is written to `.glasstrace/config.tmp`, fsynced
97
+ * to durable storage, then renamed into place; the parent directory is
98
+ * fsynced last so the rename survives an immediate crash. `rename` is
99
+ * atomic on POSIX filesystems, so readers either see the previous valid
100
+ * config or the new valid config — never a truncated or partially-written
101
+ * file (DISC-1247 Scenario 5). If any step fails, the temp file is
102
+ * cleaned up on a best-effort basis.
100
103
  */
101
104
  declare function saveCachedConfig(response: SdkInitResponse, projectRoot?: string): Promise<void>;
102
105
  /**
package/dist/index.d.ts CHANGED
@@ -88,15 +88,18 @@ declare function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey>;
88
88
  */
89
89
  declare function loadCachedConfig(projectRoot?: string): SdkInitResponse | null;
90
90
  /**
91
- * Persists the init response to `.glasstrace/config` using atomic
92
- * write-temp + rename semantics. Silently skipped when `node:fs` is
93
- * unavailable (non-Node environments). On I/O failure, logs a warning.
94
- *
95
- * Atomicity: the payload is written to `.glasstrace/config.tmp` and then
96
- * renamed into place. `rename` is atomic on POSIX filesystems, so readers
97
- * either see the previous valid config or the new valid config — never a
98
- * truncated or partially-written file (DISC-1247 Scenario 5). If the
99
- * rename fails, the temp file is cleaned up on a best-effort basis.
91
+ * Persists the init response to `.glasstrace/config` using the SDK 2.0
92
+ * atomic-write protocol (`tmp + fsync(tmp) + rename + fsync(parent)`).
93
+ * Silently skipped when `node:fs` is unavailable (non-Node environments).
94
+ * On I/O failure, logs a warning.
95
+ *
96
+ * Atomicity: the payload is written to `.glasstrace/config.tmp`, fsynced
97
+ * to durable storage, then renamed into place; the parent directory is
98
+ * fsynced last so the rename survives an immediate crash. `rename` is
99
+ * atomic on POSIX filesystems, so readers either see the previous valid
100
+ * config or the new valid config — never a truncated or partially-written
101
+ * file (DISC-1247 Scenario 5). If any step fails, the temp file is
102
+ * cleaned up on a best-effort basis.
100
103
  */
101
104
  declare function saveCachedConfig(response: SdkInitResponse, projectRoot?: string): Promise<void>;
102
105
  /**
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  registerGlasstrace,
13
13
  waitForReady,
14
14
  withGlasstraceConfig
15
- } from "./chunk-Z35HKVSO.js";
15
+ } from "./chunk-6RIH6SFM.js";
16
16
  import {
17
17
  GlasstraceSpanProcessor,
18
18
  SdkError,
@@ -27,7 +27,7 @@ import {
27
27
  performInit,
28
28
  saveCachedConfig,
29
29
  sendInitRequest
30
- } from "./chunk-C567H5EQ.js";
30
+ } from "./chunk-JKI4OCFV.js";
31
31
  import {
32
32
  isAnonymousMode,
33
33
  isProductionDisabled,
@@ -37,7 +37,7 @@ import {
37
37
  import {
38
38
  getOrCreateAnonKey,
39
39
  readAnonKey
40
- } from "./chunk-3LILTM3T.js";
40
+ } from "./chunk-TWTWRJ25.js";
41
41
  import {
42
42
  deriveSessionId
43
43
  } from "./chunk-X5MAXP5T.js";
@@ -17495,6 +17495,208 @@ function sleep(ms, scheduler, signal) {
17495
17495
  // src/mcp-runtime.ts
17496
17496
  var import_node_crypto = require("node:crypto");
17497
17497
  init_dist();
17498
+
17499
+ // src/atomic-write.ts
17500
+ function parentDir(filePath) {
17501
+ const lastSlash = filePath.lastIndexOf("/");
17502
+ const lastBackslash = filePath.lastIndexOf("\\");
17503
+ const lastSep = Math.max(lastSlash, lastBackslash);
17504
+ if (lastSep < 0) return ".";
17505
+ if (lastSep === 0) return filePath.slice(0, 1);
17506
+ return filePath.slice(0, lastSep);
17507
+ }
17508
+ var PARENT_FSYNC_SWALLOWED_CODES = /* @__PURE__ */ new Set([
17509
+ "EISDIR",
17510
+ "EINVAL",
17511
+ "EPERM",
17512
+ "ENOTSUP"
17513
+ ]);
17514
+ function errnoCodeOf(err) {
17515
+ if (err === null || typeof err !== "object") return void 0;
17516
+ const code = err.code;
17517
+ return typeof code === "string" ? code : void 0;
17518
+ }
17519
+ function defaultTmpPath(targetPath) {
17520
+ return `${targetPath}.tmp`;
17521
+ }
17522
+ var fsPromisesCache;
17523
+ var fsSyncCache;
17524
+ async function loadFsPromises() {
17525
+ if (fsPromisesCache !== void 0) {
17526
+ if (fsPromisesCache === null) {
17527
+ throw new Error(
17528
+ "node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
17529
+ );
17530
+ }
17531
+ return fsPromisesCache;
17532
+ }
17533
+ try {
17534
+ fsPromisesCache = await import("node:fs/promises");
17535
+ return fsPromisesCache;
17536
+ } catch {
17537
+ fsPromisesCache = null;
17538
+ throw new Error(
17539
+ "node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
17540
+ );
17541
+ }
17542
+ }
17543
+ function loadFsSync() {
17544
+ if (fsSyncCache !== void 0) {
17545
+ if (fsSyncCache === null) {
17546
+ throw new Error(
17547
+ "node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
17548
+ );
17549
+ }
17550
+ return fsSyncCache;
17551
+ }
17552
+ try {
17553
+ fsSyncCache = require("node:fs");
17554
+ return fsSyncCache;
17555
+ } catch {
17556
+ fsSyncCache = null;
17557
+ throw new Error(
17558
+ "node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
17559
+ );
17560
+ }
17561
+ }
17562
+ async function atomicWriteFile(targetPath, payload, options = {}) {
17563
+ return atomicWriteFileWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
17564
+ }
17565
+ async function atomicWriteFileWithTmp(targetPath, tmpPath, payload, options = {}) {
17566
+ const mode = options.mode ?? 384;
17567
+ const encoding = options.encoding ?? "utf-8";
17568
+ const fsp = await loadFsPromises();
17569
+ let handle = null;
17570
+ try {
17571
+ if (typeof payload === "string") {
17572
+ await fsp.writeFile(tmpPath, payload, { encoding, mode });
17573
+ } else {
17574
+ await fsp.writeFile(tmpPath, payload, { mode });
17575
+ }
17576
+ await fsp.chmod(tmpPath, mode);
17577
+ handle = await fsp.open(tmpPath, "r");
17578
+ await handle.sync();
17579
+ await handle.close();
17580
+ handle = null;
17581
+ await fsp.rename(tmpPath, targetPath);
17582
+ } catch (err) {
17583
+ if (handle !== null) {
17584
+ try {
17585
+ await handle.close();
17586
+ } catch {
17587
+ }
17588
+ }
17589
+ await removeTmpResidueAsync(fsp, tmpPath);
17590
+ throw err;
17591
+ }
17592
+ await fsyncParentDirAsync(targetPath, fsp);
17593
+ }
17594
+ async function removeTmpResidueAsync(fsp, tmpPath) {
17595
+ try {
17596
+ await fsp.unlink(tmpPath);
17597
+ return;
17598
+ } catch (err) {
17599
+ const code = errnoCodeOf(err);
17600
+ if (code !== "EISDIR" && code !== "EPERM") {
17601
+ return;
17602
+ }
17603
+ }
17604
+ try {
17605
+ await fsp.rmdir(tmpPath);
17606
+ } catch {
17607
+ }
17608
+ }
17609
+ async function fsyncParentDirAsync(targetPath, fsp) {
17610
+ const parent = parentDir(targetPath);
17611
+ let handle = null;
17612
+ try {
17613
+ handle = await fsp.open(parent, "r");
17614
+ await handle.sync();
17615
+ } catch (err) {
17616
+ const code = errnoCodeOf(err);
17617
+ if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
17618
+ return;
17619
+ }
17620
+ throw err;
17621
+ } finally {
17622
+ if (handle !== null) {
17623
+ try {
17624
+ await handle.close();
17625
+ } catch {
17626
+ }
17627
+ }
17628
+ }
17629
+ }
17630
+ function atomicWriteFileSync(targetPath, payload, options = {}) {
17631
+ atomicWriteFileSyncWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
17632
+ }
17633
+ function atomicWriteFileSyncWithTmp(targetPath, tmpPath, payload, options = {}) {
17634
+ const mode = options.mode ?? 384;
17635
+ const encoding = options.encoding ?? "utf-8";
17636
+ const fs4 = loadFsSync();
17637
+ let fd = null;
17638
+ try {
17639
+ if (typeof payload === "string") {
17640
+ fs4.writeFileSync(tmpPath, payload, { encoding, mode });
17641
+ } else {
17642
+ fs4.writeFileSync(tmpPath, payload, { mode });
17643
+ }
17644
+ fs4.chmodSync(tmpPath, mode);
17645
+ fd = fs4.openSync(tmpPath, "r");
17646
+ fs4.fsyncSync(fd);
17647
+ fs4.closeSync(fd);
17648
+ fd = null;
17649
+ fs4.renameSync(tmpPath, targetPath);
17650
+ } catch (err) {
17651
+ if (fd !== null) {
17652
+ try {
17653
+ fs4.closeSync(fd);
17654
+ } catch {
17655
+ }
17656
+ }
17657
+ removeTmpResidueSync(fs4, tmpPath);
17658
+ throw err;
17659
+ }
17660
+ fsyncParentDirSyncWithFs(targetPath, fs4);
17661
+ }
17662
+ function removeTmpResidueSync(fs4, tmpPath) {
17663
+ try {
17664
+ fs4.unlinkSync(tmpPath);
17665
+ return;
17666
+ } catch (err) {
17667
+ const code = errnoCodeOf(err);
17668
+ if (code !== "EISDIR" && code !== "EPERM") {
17669
+ return;
17670
+ }
17671
+ }
17672
+ try {
17673
+ fs4.rmdirSync(tmpPath);
17674
+ } catch {
17675
+ }
17676
+ }
17677
+ function fsyncParentDirSyncWithFs(targetPath, fs4) {
17678
+ const parent = parentDir(targetPath);
17679
+ let fd = null;
17680
+ try {
17681
+ fd = fs4.openSync(parent, "r");
17682
+ fs4.fsyncSync(fd);
17683
+ } catch (err) {
17684
+ const code = errnoCodeOf(err);
17685
+ if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
17686
+ return;
17687
+ }
17688
+ throw err;
17689
+ } finally {
17690
+ if (fd !== null) {
17691
+ try {
17692
+ fs4.closeSync(fd);
17693
+ } catch {
17694
+ }
17695
+ }
17696
+ }
17697
+ }
17698
+
17699
+ // src/mcp-runtime.ts
17498
17700
  var MCP_ENDPOINT = "https://api.glasstrace.dev/mcp";
17499
17701
  var fsPathCache2;
17500
17702
  async function loadFsPath2() {
@@ -17708,7 +17910,6 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
17708
17910
  if (!modules) return { action: "absent" };
17709
17911
  const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR2);
17710
17912
  const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);
17711
- const tmpPath = configPath + ".tmp";
17712
17913
  let existing;
17713
17914
  try {
17714
17915
  existing = await modules.fs.readFile(configPath, "utf-8");
@@ -17725,18 +17926,12 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
17725
17926
  }
17726
17927
  const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);
17727
17928
  try {
17728
- await modules.fs.writeFile(tmpPath, replacement, { mode: 384 });
17729
- await modules.fs.chmod(tmpPath, 384);
17730
- await modules.fs.rename(tmpPath, configPath);
17929
+ await atomicWriteFile(configPath, replacement, { mode: 384 });
17731
17930
  await writeMcpMarker(projectRoot, {
17732
17931
  credentialSource: effective.source,
17733
17932
  credentialHash: identityFingerprint(effective.key)
17734
17933
  });
17735
17934
  } catch {
17736
- try {
17737
- await modules.fs.unlink(tmpPath);
17738
- } catch {
17739
- }
17740
17935
  return { action: "preserved" };
17741
17936
  }
17742
17937
  emitRefreshNudge(effective.source);
@@ -17809,7 +18004,6 @@ async function saveCachedConfig(response, projectRoot) {
17809
18004
  const root = projectRoot ?? process.cwd();
17810
18005
  const dirPath = modules.path.join(root, GLASSTRACE_DIR3);
17811
18006
  const configPath = modules.path.join(dirPath, CONFIG_FILE);
17812
- const tmpPath = `${configPath}.tmp`;
17813
18007
  try {
17814
18008
  await modules.fs.mkdir(dirPath, { recursive: true, mode: 448 });
17815
18009
  await modules.fs.chmod(dirPath, 448);
@@ -17817,20 +18011,10 @@ async function saveCachedConfig(response, projectRoot) {
17817
18011
  response,
17818
18012
  cachedAt: Date.now()
17819
18013
  };
17820
- await modules.fs.writeFile(tmpPath, JSON.stringify(cached2), {
18014
+ await atomicWriteFile(configPath, JSON.stringify(cached2), {
17821
18015
  encoding: "utf-8",
17822
18016
  mode: 384
17823
18017
  });
17824
- try {
17825
- await modules.fs.chmod(tmpPath, 384);
17826
- await modules.fs.rename(tmpPath, configPath);
17827
- } catch (renameErr) {
17828
- try {
17829
- await modules.fs.unlink(tmpPath);
17830
- } catch {
17831
- }
17832
- throw renameErr;
17833
- }
17834
18018
  await modules.fs.chmod(configPath, 384);
17835
18019
  } catch (err) {
17836
18020
  console.warn(
@@ -22076,8 +22260,51 @@ async function runRegistrationPath(config2, sessionManager) {
22076
22260
  // src/context-manager.ts
22077
22261
  var import_node_async_hooks = require("node:async_hooks");
22078
22262
  init_esm();
22263
+ var GLASSTRACE_BRAND = 1;
22264
+ var GUARD = /* @__PURE__ */ Symbol.for("glasstrace.context-manager.installed");
22265
+ var OTEL_API_KEY = /* @__PURE__ */ Symbol.for("opentelemetry.js.api.1");
22266
+ function isOtelContextManager(value) {
22267
+ if (typeof value !== "object" || value === null) return false;
22268
+ const candidate = value;
22269
+ return typeof candidate.active === "function" && typeof candidate.with === "function" && typeof candidate.bind === "function" && typeof candidate.enable === "function" && typeof candidate.disable === "function";
22270
+ }
22271
+ function isInstallationRecord(value) {
22272
+ if (typeof value !== "object" || value === null) return false;
22273
+ const candidate = value;
22274
+ if (candidate.glasstraceContextManagerBrand !== GLASSTRACE_BRAND) return false;
22275
+ return candidate.manager === null || isOtelContextManager(candidate.manager);
22276
+ }
22277
+ function getOtelRegisteredContextManager() {
22278
+ const otelSlot = globalThis[OTEL_API_KEY];
22279
+ if (typeof otelSlot !== "object" || otelSlot === null) return void 0;
22280
+ const ctx = otelSlot.context;
22281
+ return isOtelContextManager(ctx) ? ctx : void 0;
22282
+ }
22079
22283
  function installContextManager() {
22080
22284
  try {
22285
+ const slot = globalThis;
22286
+ const existing = slot[GUARD];
22287
+ const otelCurrent = getOtelRegisteredContextManager();
22288
+ if (isInstallationRecord(existing) && existing.manager !== null && existing.manager === otelCurrent) {
22289
+ return true;
22290
+ }
22291
+ if (isInstallationRecord(existing) && existing.manager === null && otelCurrent !== void 0) {
22292
+ return false;
22293
+ }
22294
+ if (isInstallationRecord(existing) && existing.manager !== null) {
22295
+ const reSuccess = context.setGlobalContextManager(existing.manager);
22296
+ if (!reSuccess) {
22297
+ console.warn(
22298
+ "[glasstrace] Another context manager is already registered. Trace context propagation may not work as expected."
22299
+ );
22300
+ }
22301
+ const reRecord = {
22302
+ glasstraceContextManagerBrand: GLASSTRACE_BRAND,
22303
+ manager: reSuccess ? existing.manager : null
22304
+ };
22305
+ slot[GUARD] = reRecord;
22306
+ return reSuccess;
22307
+ }
22081
22308
  const als = new import_node_async_hooks.AsyncLocalStorage();
22082
22309
  const contextManager = {
22083
22310
  active: () => als.getStore() ?? ROOT_CONTEXT,
@@ -22098,6 +22325,11 @@ function installContextManager() {
22098
22325
  "[glasstrace] Another context manager is already registered. Trace context propagation may not work as expected."
22099
22326
  );
22100
22327
  }
22328
+ const record2 = {
22329
+ glasstraceContextManagerBrand: GLASSTRACE_BRAND,
22330
+ manager: success2 ? contextManager : null
22331
+ };
22332
+ slot[GUARD] = record2;
22101
22333
  return success2;
22102
22334
  } catch {
22103
22335
  return false;
@@ -22287,12 +22519,10 @@ function writeStateNow() {
22287
22519
  };
22288
22520
  const dir = (0, import_node_path.join)(_projectRoot, ".glasstrace");
22289
22521
  const filePath = (0, import_node_path.join)(dir, "runtime-state.json");
22290
- const tmpPath = (0, import_node_path.join)(dir, "runtime-state.json.tmp");
22291
22522
  (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
22292
- (0, import_node_fs.writeFileSync)(tmpPath, JSON.stringify(runtimeState, null, 2) + "\n", {
22523
+ atomicWriteFileSync(filePath, JSON.stringify(runtimeState, null, 2) + "\n", {
22293
22524
  mode: 384
22294
22525
  });
22295
- (0, import_node_fs.renameSync)(tmpPath, filePath);
22296
22526
  } catch (err) {
22297
22527
  sdkLog(
22298
22528
  "warn",
@@ -22331,7 +22561,7 @@ function registerGlasstrace(options) {
22331
22561
  setCoreState(CoreState.REGISTERING);
22332
22562
  startRuntimeStateWriter({
22333
22563
  projectRoot: process.cwd(),
22334
- sdkVersion: "1.1.2"
22564
+ sdkVersion: "1.2.0"
22335
22565
  });
22336
22566
  const config2 = resolveConfig(options);
22337
22567
  if (config2.verbose) {
@@ -22497,8 +22727,8 @@ async function backgroundInit(config2, anonKeyForInit, generation) {
22497
22727
  if (config2.verbose) {
22498
22728
  console.info("[glasstrace] Background init firing.");
22499
22729
  }
22500
- const healthReport = collectHealthReport("1.1.2");
22501
- const initResult = await performInit(config2, anonKeyForInit, "1.1.2", healthReport);
22730
+ const healthReport = collectHealthReport("1.2.0");
22731
+ const initResult = await performInit(config2, anonKeyForInit, "1.2.0", healthReport);
22502
22732
  if (generation !== registrationGeneration) return;
22503
22733
  const currentState = getCoreState();
22504
22734
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -22521,7 +22751,7 @@ async function backgroundInit(config2, anonKeyForInit, generation) {
22521
22751
  }
22522
22752
  maybeInstallConsoleCapture();
22523
22753
  if (didLastInitSucceed()) {
22524
- startHeartbeat(config2, anonKeyForInit, "1.1.2", generation, (newApiKey, accountId) => {
22754
+ startHeartbeat(config2, anonKeyForInit, "1.2.0", generation, (newApiKey, accountId) => {
22525
22755
  setAuthState(AuthState.CLAIMING);
22526
22756
  emitLifecycleEvent("auth:claim_started", { accountId });
22527
22757
  setResolvedApiKey(newApiKey);