retrace-sdk 0.16.1 → 0.16.2
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 +11 -3
- package/dist/config.js +32 -1
- package/dist/telemetry.js +1 -1
- package/dist/transport.js +24 -14
- package/package.json +1 -1
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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: ["
|
|
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,6 +21,32 @@ 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
52
|
throw new Error("Invalid Retrace API key. Keys must start with 'rt_'. Get yours at https://retraceai.tech/settings");
|
|
@@ -28,6 +55,7 @@ export function configure(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;
|
|
@@ -42,6 +70,9 @@ export function requireApiKey() {
|
|
|
42
70
|
if (!config.apiKey) {
|
|
43
71
|
throw new Error("Retrace API key required. Call configure({ apiKey: 'rt_...' }) or set RETRACE_API_KEY. Get yours at https://retraceai.tech/settings");
|
|
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.
|
|
17
|
+
const SDK_VERSION = "0.16.2";
|
|
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.
|
|
5
|
+
const CLIENT_ID = "typescript-sdk/0.16.2";
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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";
|