retrace-sdk 0.16.1 → 0.16.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/dist/config.d.ts CHANGED
@@ -12,10 +12,18 @@ export interface Config {
12
12
  * holds an open socket and always surfaces upload errors); "ws" forces WebSocket. */
13
13
  transport: "auto" | "ws" | "http";
14
14
  /** Replay safety. When a deterministic replay hits a cassette MISS (no recorded entry for a
15
- * call), the SDK otherwise falls through to a REAL provider call (live cost + non-determinism).
16
- * With strictReplay=true the SDK throws instead (fail-closed). Default false: warn + fall through.
17
- * Env: RETRACE_STRICT_REPLAY. */
15
+ * call), the SDK can either throw (fail-closed) or fall through to a REAL provider call (live cost
16
+ * + non-determinism, driven by a server-supplied cassette). Default TRUE (fail-closed): the SDK
17
+ * throws on a miss. Set strictReplay:false (or RETRACE_STRICT_REPLAY=false) to opt back into the
18
+ * legacy warn + fall-through behavior. Env: RETRACE_STRICT_REPLAY. */
18
19
  strictReplay: boolean;
20
+ /** C2: honor SERVER-INITIATED resume/replay commands — dashboard-driven cascade replay that
21
+ * re-executes YOUR registered (resumable) function in THIS process with server-supplied input and
22
+ * cassette. Default FALSE: the SDK ignores those frames unless you explicitly opt in, because
23
+ * acting on them means trusting the server (and the transport) to invoke code in your environment
24
+ * with attacker-influenceable arguments. Recording, and server-side forking, are unaffected.
25
+ * Env: RETRACE_ALLOW_REMOTE_REPLAY. */
26
+ allowRemoteReplay: boolean;
19
27
  /** Called with a STRUCTURED signal when the server signals credits_exhausted | rate_limited |
20
28
  * halt | error. Branch on `signal.code`; use `signal.retryable`/`signal.fatal` to decide
21
29
  * behavior. Defaults to a throttled console warning so signals are never silently dropped. */
package/dist/config.js CHANGED
@@ -11,7 +11,8 @@ const config = {
11
11
  sampleRate: parseFloat(env.RETRACE_SAMPLE_RATE || "1"),
12
12
  sampleSeed: env.RETRACE_SAMPLE_SEED || undefined,
13
13
  transport: (["auto", "ws", "http"].includes(env.RETRACE_TRANSPORT || "") ? env.RETRACE_TRANSPORT : "auto"),
14
- strictReplay: ["true", "1"].includes((env.RETRACE_STRICT_REPLAY || "").toLowerCase()),
14
+ strictReplay: !["false", "0", "no"].includes((env.RETRACE_STRICT_REPLAY || "").toLowerCase()),
15
+ allowRemoteReplay: ["true", "1", "yes"].includes((env.RETRACE_ALLOW_REMOTE_REPLAY || "").toLowerCase()),
15
16
  maxStepsPerRun: env.RETRACE_MAX_STEPS_PER_RUN ? parseInt(env.RETRACE_MAX_STEPS_PER_RUN, 10) : undefined,
16
17
  maxTokensPerRun: env.RETRACE_MAX_TOKENS_PER_RUN ? parseInt(env.RETRACE_MAX_TOKENS_PER_RUN, 10) : undefined,
17
18
  maxUsdPerRun: env.RETRACE_MAX_USD_PER_RUN ? parseFloat(env.RETRACE_MAX_USD_PER_RUN) : undefined,
@@ -20,14 +21,41 @@ const config = {
20
21
  environment: env.RETRACE_ENVIRONMENT || undefined,
21
22
  };
22
23
  config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
24
+ function isLoopbackHost(host) {
25
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
26
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
27
+ }
28
+ /**
29
+ * C3: refuse cleartext transports for non-loopback hosts. Over http:// → ws:// the one-time auth
30
+ * frame sends the API key in plaintext AND every server command frame (resume/replay/halt) is
31
+ * injectable on the wire — which is the transport half of the C2 command-injection chain. https://
32
+ * (and http://localhost for local dev) are allowed; anything else hard-errors before any bytes,
33
+ * including the API key, leave the process.
34
+ */
35
+ function assertSecureBaseUrl(baseUrl) {
36
+ let u;
37
+ try {
38
+ u = new URL(baseUrl);
39
+ }
40
+ catch {
41
+ throw new Error(`Retrace: invalid base URL ${JSON.stringify(baseUrl)} (set RETRACE_BASE_URL or configure({ baseUrl }) to an https:// URL).`);
42
+ }
43
+ if (u.protocol === "https:")
44
+ return;
45
+ if (u.protocol === "http:" && isLoopbackHost(u.hostname))
46
+ return; // local dev only
47
+ throw new Error(`Retrace: refusing insecure base URL ${JSON.stringify(baseUrl)}. Use https:// (http:// is allowed only for localhost). ` +
48
+ "Over cleartext the API key is transmitted in plaintext and server replay/resume command frames can be injected on the wire.");
49
+ }
23
50
  export function configure(opts) {
24
51
  if (opts.apiKey && (!opts.apiKey.startsWith("rt_") || opts.apiKey.startsWith("rt_live_") || opts.apiKey.startsWith("rt_test_"))) {
25
- throw new Error("Invalid Retrace API key. Keys must start with 'rt_'. Get yours at https://retraceai.tech/settings");
52
+ throw new Error("Invalid Retrace API key. Keys must start with 'rt_'. Get yours at https://retraceai.tech/workspace/api-keys");
26
53
  }
27
54
  Object.assign(config, opts);
28
55
  if (opts.baseUrl && !opts.wsUrl) {
29
56
  config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
30
57
  }
58
+ assertSecureBaseUrl(config.baseUrl);
31
59
  // Eagerly install provider interceptors so clients constructed AFTER configure() are patched.
32
60
  // @google/genai binds generateContent as an own instance property, and our accessor only wraps
33
61
  // instances built after install — so install must precede client construction. Fire-and-forget;
@@ -40,8 +68,11 @@ export function configure(opts) {
40
68
  }
41
69
  export function requireApiKey() {
42
70
  if (!config.apiKey) {
43
- throw new Error("Retrace API key required. Call configure({ apiKey: 'rt_...' }) or set RETRACE_API_KEY. Get yours at https://retraceai.tech/settings");
71
+ throw new Error("Retrace API key required. Call configure({ apiKey: 'rt_...' }) or set RETRACE_API_KEY. Get yours at https://retraceai.tech/workspace/api-keys");
44
72
  }
73
+ // Validate the transport is encrypted before the key is ever used on the wire (covers env-only
74
+ // config that never called configure()).
75
+ assertSecureBaseUrl(config.baseUrl);
45
76
  return config.apiKey;
46
77
  }
47
78
  export function getConfig() {
package/dist/telemetry.js CHANGED
@@ -14,7 +14,7 @@ import { getConfig } from "./config.js";
14
14
  const ANON_ID = Math.random().toString(16).slice(2, 18);
15
15
  const DISABLED = new Set(["0", "false", "no", "off"]);
16
16
  // Keep in sync with package.json version.
17
- const SDK_VERSION = "0.16.1";
17
+ const SDK_VERSION = "0.16.3";
18
18
  function enabled() {
19
19
  return !DISABLED.has((process.env.RETRACE_TELEMETRY ?? "1").trim().toLowerCase());
20
20
  }
package/dist/transport.js CHANGED
@@ -2,7 +2,7 @@ import { getConfig } from "./config.js";
2
2
  import { classifyServerSignal } from "./errors.js";
3
3
  // Client identifier sent on every request so the backend can attribute SDK usage/version.
4
4
  // Keep in sync with package.json on release.
5
- const CLIENT_ID = "typescript-sdk/0.16.1";
5
+ const CLIENT_ID = "typescript-sdk/0.16.3";
6
6
  // ─── Runtime-agnostic WebSocket ──────────────────────────────────────────────
7
7
  // Prefer the global Web `WebSocket` (Node 20+, Bun, Deno, browsers, and every edge runtime); fall
8
8
  // back to the OPTIONAL `ws` package only on older Node that lacks a global. Both expose the standard
@@ -103,19 +103,29 @@ export class WSTransport {
103
103
  else if (msg.type === "error") {
104
104
  this.surfaceSignal(classifyServerSignal("error", msg.error));
105
105
  }
106
- else if (msg.type === "resume") {
107
- import("./resume.js").then(({ parseResumeMessage, handleResume }) => {
108
- const cmd = parseResumeMessage(msg);
109
- if (cmd)
110
- handleResume(cmd);
111
- });
112
- }
113
- else if (msg.type === "replay") {
114
- import("./replay.js").then(({ parseReplayMessage, handleReplay }) => {
115
- const cmd = parseReplayMessage(msg);
116
- if (cmd)
117
- handleReplay(cmd);
118
- });
106
+ else if (msg.type === "resume" || msg.type === "replay") {
107
+ // C2: server-initiated re-execution invokes the user's registered function in THIS process
108
+ // with server-supplied input/args/cassette. Off by default — only act on these frames when
109
+ // the developer explicitly opted in, so a compromised/misrouted server (or an injected
110
+ // frame) can't drive code execution here.
111
+ if (!getConfig().allowRemoteReplay) {
112
+ this.throttledSignalWarn(`remote_blocked:${msg.type}`, `[retrace] ignoring server "${msg.type}" command — remote-controlled re-execution is disabled. Set allowRemoteReplay:true (or RETRACE_ALLOW_REMOTE_REPLAY=true) to opt in.`);
113
+ return;
114
+ }
115
+ if (msg.type === "resume") {
116
+ import("./resume.js").then(({ parseResumeMessage, handleResume }) => {
117
+ const cmd = parseResumeMessage(msg);
118
+ if (cmd)
119
+ handleResume(cmd);
120
+ });
121
+ }
122
+ else {
123
+ import("./replay.js").then(({ parseReplayMessage, handleReplay }) => {
124
+ const cmd = parseReplayMessage(msg);
125
+ if (cmd)
126
+ handleReplay(cmd);
127
+ });
128
+ }
119
129
  }
120
130
  else if (msg.type === "halt") {
121
131
  const reason = msg.data?.reason || "Guardrail triggered";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.16.1",
3
+ "version": "0.16.3",
4
4
  "description": "The execution replay engine for AI agents. Record, replay, fork, and share agent executions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",