@sudobility/sider_lib 0.0.8 → 0.0.10

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.
@@ -0,0 +1,2 @@
1
+ /** Whether a header names request tracing that can be dropped rather than resolved. */
2
+ export declare function isCorrelationHeader(name: string): boolean;
@@ -0,0 +1,28 @@
1
+ // Headers that identify a REQUEST, not a person.
2
+ //
3
+ // `x-ebay-c-correlation-session` is a per-page correlation id: it ties a page's
4
+ // calls together in eBay's own logs and carries no authority whatsoever. It was
5
+ // treated as a credential because its name ends in "session", and the fallout
6
+ // was worse than noise — nothing local holds such a value, so egress could not
7
+ // resolve it, the call was blocked, and the agent told a signed-in user to sign
8
+ // in. The actual eBay session is a cookie the browser attaches by itself.
9
+ //
10
+ // A correlation header is safe to omit. The server mints its own when it is
11
+ // missing, which is exactly what happens on a request the site makes itself.
12
+ /** Names that mark a header as request-scoped tracing rather than a credential. */
13
+ const CORRELATION_RE = /correlation|traceparent|tracestate|\btrace\b|request[-_]?id|\breq[-_]?id\b|\bspan\b|\bray\b|\bb3\b/i;
14
+ /**
15
+ * Names that are unambiguously credentials, whatever else they contain.
16
+ *
17
+ * Checked first so a hypothetical `x-csrf-correlation` stays a secret. Weak
18
+ * words like "session", "token" and "id" are deliberately NOT here: those are
19
+ * the ones correlation headers routinely contain, and treating them as decisive
20
+ * is the mistake being fixed.
21
+ */
22
+ const CREDENTIAL_RE = /authorization|bearer|csrf|xsrf|api[-_]?key|apikey|secret|password/i;
23
+ /** Whether a header names request tracing that can be dropped rather than resolved. */
24
+ export function isCorrelationHeader(name) {
25
+ if (CREDENTIAL_RE.test(name))
26
+ return false;
27
+ return CORRELATION_RE.test(name);
28
+ }
package/dist/execute.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // TokenMap) and substitutes it into the sentinel positions the gates already
5
5
  // proved are confined to the declared location.
6
6
  import { compileRecipe, secretSentinel } from "./recipe";
7
+ import { isCorrelationHeader } from "./correlation";
7
8
  import { evaluateGates } from "./gates";
8
9
  export function detokenizeRequest(req, resolve, slotsById) {
9
10
  let url = req.url;
@@ -15,8 +16,21 @@ export function detokenizeRequest(req, resolve, slotsById) {
15
16
  if (slot && (slot.role === "auto_cookie" || slot.injectionLocation.at === "cookie"))
16
17
  continue;
17
18
  const value = resolve(slotId);
18
- if (value === undefined)
19
+ if (value === undefined) {
20
+ // A correlation header that cannot be resolved is dropped, not fatal.
21
+ // Slots minted before these were recognised still sit in the registry,
22
+ // and every one of them would otherwise block a call the user's own
23
+ // session can make — the site issues a fresh id when the header is absent.
24
+ if (slot && slot.injectionLocation.at === "header" && isCorrelationHeader(slot.injectionLocation.name)) {
25
+ delete headers[slot.injectionLocation.name];
26
+ const sen = secretSentinel(slotId);
27
+ url = url.split(sen).join("");
28
+ if (bodyStr !== undefined)
29
+ bodyStr = bodyStr.split(sen).join("");
30
+ continue;
31
+ }
19
32
  throw new Error(`Unresolved secret slot at egress: ${slotId}`);
33
+ }
20
34
  const sen = secretSentinel(slotId);
21
35
  url = url.split(sen).join(value);
22
36
  for (const k of Object.keys(headers))
@@ -109,3 +109,24 @@ test("cookie-borne slot is refused (must be auto-attached, never injected)", ()
109
109
  if (!out.ok)
110
110
  expect(out.gate).toBe("injection_location");
111
111
  });
112
+ // --- correlation headers ----------------------------------------------------
113
+ import { isCorrelationHeader } from "./correlation";
114
+ test("a correlation id is not a credential", () => {
115
+ // The header that blocked a signed-in user's call: it ends in "session", so
116
+ // the name rule claimed it, and no local store holds a per-page trace id.
117
+ expect(isCorrelationHeader("x-ebay-c-correlation-session")).toBe(true);
118
+ expect(isCorrelationHeader("x-request-id")).toBe(true);
119
+ expect(isCorrelationHeader("traceparent")).toBe(true);
120
+ });
121
+ test("real credentials are still credentials, whatever else the name says", () => {
122
+ expect(isCorrelationHeader("authorization")).toBe(false);
123
+ expect(isCorrelationHeader("cookie")).toBe(false);
124
+ expect(isCorrelationHeader("x-csrf-token")).toBe(false);
125
+ expect(isCorrelationHeader("x-api-key")).toBe(false);
126
+ // Contains a correlation word AND an unambiguous credential word: the
127
+ // credential wins, because dropping it would send an unauthenticated call.
128
+ expect(isCorrelationHeader("x-csrf-correlation")).toBe(false);
129
+ });
130
+ test("a session header with no correlation marker stays a secret", () => {
131
+ expect(isCorrelationHeader("x-session-id")).toBe(false);
132
+ });
package/dist/tokenize.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // 3. Body fields whose KEY looks credential-bearing (e.g. access_token).
11
11
  // 4. JWT-shaped strings anywhere in a body.
12
12
  // Bias is conservative-for-privacy: over-mask rather than leak.
13
+ import { isCorrelationHeader } from "./correlation";
13
14
  export function samplePlaceholder(slotId) {
14
15
  return `{{secret:${slotId}}}`;
15
16
  }
@@ -78,6 +79,11 @@ export function tokenizeObservation(raw, known) {
78
79
  continue;
79
80
  if (!SECRET_HEADER_RE.test(name))
80
81
  continue;
82
+ // A correlation id is not a credential. Minting a slot for one guarantees a
83
+ // block later: no local store holds a per-page trace id, so the call dies
84
+ // at egress for want of a value the site does not need sent.
85
+ if (isCorrelationHeader(name))
86
+ continue;
81
87
  const lname = name.toLowerCase();
82
88
  const slotId = `slot:${host}:${normalize(name)}`;
83
89
  headers[name] = samplePlaceholder(slotId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/sider_lib",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Sider business logic — recipe compiler, egress gates, tokenizer, clustering. Pure, no Chrome/DOM/server.",
5
5
  "license": "BUSL-1.1",
6
6
  "publishConfig": {
@@ -19,7 +19,7 @@
19
19
  "test": "bun test"
20
20
  },
21
21
  "dependencies": {
22
- "@sudobility/sider_types": "^0.0.10"
22
+ "@sudobility/sider_types": "^0.0.11"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/bun": "^1.3.14",
@@ -0,0 +1,30 @@
1
+ // Headers that identify a REQUEST, not a person.
2
+ //
3
+ // `x-ebay-c-correlation-session` is a per-page correlation id: it ties a page's
4
+ // calls together in eBay's own logs and carries no authority whatsoever. It was
5
+ // treated as a credential because its name ends in "session", and the fallout
6
+ // was worse than noise — nothing local holds such a value, so egress could not
7
+ // resolve it, the call was blocked, and the agent told a signed-in user to sign
8
+ // in. The actual eBay session is a cookie the browser attaches by itself.
9
+ //
10
+ // A correlation header is safe to omit. The server mints its own when it is
11
+ // missing, which is exactly what happens on a request the site makes itself.
12
+
13
+ /** Names that mark a header as request-scoped tracing rather than a credential. */
14
+ const CORRELATION_RE = /correlation|traceparent|tracestate|\btrace\b|request[-_]?id|\breq[-_]?id\b|\bspan\b|\bray\b|\bb3\b/i;
15
+
16
+ /**
17
+ * Names that are unambiguously credentials, whatever else they contain.
18
+ *
19
+ * Checked first so a hypothetical `x-csrf-correlation` stays a secret. Weak
20
+ * words like "session", "token" and "id" are deliberately NOT here: those are
21
+ * the ones correlation headers routinely contain, and treating them as decisive
22
+ * is the mistake being fixed.
23
+ */
24
+ const CREDENTIAL_RE = /authorization|bearer|csrf|xsrf|api[-_]?key|apikey|secret|password/i;
25
+
26
+ /** Whether a header names request tracing that can be dropped rather than resolved. */
27
+ export function isCorrelationHeader(name: string): boolean {
28
+ if (CREDENTIAL_RE.test(name)) return false;
29
+ return CORRELATION_RE.test(name);
30
+ }
package/src/execute.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { CompiledRequest, SecretSlot } from "@sudobility/sider_types";
8
8
  import { compileRecipe, secretSentinel } from "./recipe";
9
+ import { isCorrelationHeader } from "./correlation";
9
10
  import { evaluateGates } from "./gates";
10
11
  import type { ToolSpec } from "@sudobility/sider_types";
11
12
  import type { CompileContext } from "./recipe";
@@ -37,7 +38,20 @@ export function detokenizeRequest(
37
38
  if (slot && (slot.role === "auto_cookie" || slot.injectionLocation.at === "cookie")) continue;
38
39
 
39
40
  const value = resolve(slotId);
40
- if (value === undefined) throw new Error(`Unresolved secret slot at egress: ${slotId}`);
41
+ if (value === undefined) {
42
+ // A correlation header that cannot be resolved is dropped, not fatal.
43
+ // Slots minted before these were recognised still sit in the registry,
44
+ // and every one of them would otherwise block a call the user's own
45
+ // session can make — the site issues a fresh id when the header is absent.
46
+ if (slot && slot.injectionLocation.at === "header" && isCorrelationHeader(slot.injectionLocation.name)) {
47
+ delete headers[slot.injectionLocation.name];
48
+ const sen = secretSentinel(slotId);
49
+ url = url.split(sen).join("");
50
+ if (bodyStr !== undefined) bodyStr = bodyStr.split(sen).join("");
51
+ continue;
52
+ }
53
+ throw new Error(`Unresolved secret slot at egress: ${slotId}`);
54
+ }
41
55
 
42
56
  const sen = secretSentinel(slotId);
43
57
  url = url.split(sen).join(value);
package/src/gates.test.ts CHANGED
@@ -122,3 +122,29 @@ test("cookie-borne slot is refused (must be auto-attached, never injected)", ()
122
122
  expect(out.ok).toBe(false);
123
123
  if (!out.ok) expect(out.gate).toBe("injection_location");
124
124
  });
125
+
126
+ // --- correlation headers ----------------------------------------------------
127
+
128
+ import { isCorrelationHeader } from "./correlation";
129
+
130
+ test("a correlation id is not a credential", () => {
131
+ // The header that blocked a signed-in user's call: it ends in "session", so
132
+ // the name rule claimed it, and no local store holds a per-page trace id.
133
+ expect(isCorrelationHeader("x-ebay-c-correlation-session")).toBe(true);
134
+ expect(isCorrelationHeader("x-request-id")).toBe(true);
135
+ expect(isCorrelationHeader("traceparent")).toBe(true);
136
+ });
137
+
138
+ test("real credentials are still credentials, whatever else the name says", () => {
139
+ expect(isCorrelationHeader("authorization")).toBe(false);
140
+ expect(isCorrelationHeader("cookie")).toBe(false);
141
+ expect(isCorrelationHeader("x-csrf-token")).toBe(false);
142
+ expect(isCorrelationHeader("x-api-key")).toBe(false);
143
+ // Contains a correlation word AND an unambiguous credential word: the
144
+ // credential wins, because dropping it would send an unauthenticated call.
145
+ expect(isCorrelationHeader("x-csrf-correlation")).toBe(false);
146
+ });
147
+
148
+ test("a session header with no correlation marker stays a secret", () => {
149
+ expect(isCorrelationHeader("x-session-id")).toBe(false);
150
+ });
package/src/tokenize.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  // 4. JWT-shaped strings anywhere in a body.
12
12
  // Bias is conservative-for-privacy: over-mask rather than leak.
13
13
 
14
+ import { isCorrelationHeader } from "./correlation";
14
15
  import type {
15
16
  HttpMethod,
16
17
  InjectionLocation,
@@ -125,6 +126,10 @@ export function tokenizeObservation(raw: RawObservation, known: KnownSecret[]):
125
126
  const val = headers[name];
126
127
  if (typeof val !== "string" || !val || val.startsWith("{{secret:")) continue;
127
128
  if (!SECRET_HEADER_RE.test(name)) continue;
129
+ // A correlation id is not a credential. Minting a slot for one guarantees a
130
+ // block later: no local store holds a per-page trace id, so the call dies
131
+ // at egress for want of a value the site does not need sent.
132
+ if (isCorrelationHeader(name)) continue;
128
133
  const lname = name.toLowerCase();
129
134
  const slotId = `slot:${host}:${normalize(name)}`;
130
135
  headers[name] = samplePlaceholder(slotId);