@foam-ai/node 0.1.0-alpha.10 → 0.1.0-alpha.12

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/README.md CHANGED
@@ -181,7 +181,9 @@ init({
181
181
  ```
182
182
 
183
183
  `beforeSend` can edit or drop spans and logs before Foam exports them. Return
184
- the event to send it or `null` to drop it.
184
+ the event to send it or `null` to drop it. Span events carry `name`,
185
+ `attributes`, `events` (span events, e.g. recorded exceptions), and `links`;
186
+ log events carry `body`, `severityText`, and `attributes`.
185
187
 
186
188
  ```js
187
189
  init({
@@ -259,10 +261,10 @@ const loggerProvider = new LoggerProvider({
259
261
  logs.setGlobalLoggerProvider(loggerProvider);
260
262
  ```
261
263
 
262
- Both ingest helpers accept the same `redact` and `beforeSend` options as
263
- `init()`.
264
+ All ingest helpers accept the same `redact` option as `init()`; the span and
265
+ log helpers also accept `beforeSend`.
264
266
 
265
- ### `createFoamIngestMetricReader(name, environment, token)`
267
+ ### `createFoamIngestMetricReader(name, environment, token, options?)`
266
268
 
267
269
  The helper returns a metric reader but does not attach it to the application's
268
270
  provider. Include it in the `readers` option when constructing `MeterProvider`.
@@ -572,11 +574,16 @@ export wherever `getState().signals.traces` points.
572
574
  ## Redaction
573
575
 
574
576
  Foam always masks common credentials in captured request and response data,
575
- URLs, and exported span and log fields. Add application-specific keys with the
576
- `redact` option shown under `init(options)`.
577
+ URLs, and exported span and log fields. The built-in floor matches both exact
578
+ well-known names and any key containing a sensitive term (`legacy_api_key_2`,
579
+ `stripeToken`), so credential-bearing keys do not need to be enumerated. Add
580
+ application-specific keys with the `redact` option shown under `init(options)`;
581
+ customer keys match exactly after normalization.
577
582
 
578
- Redaction applies to Foam's exports, including ingest helpers. It does not
579
- modify telemetry sent through other exporters.
583
+ Redaction applies to everything Foam exports span, span event, and link
584
+ attributes, log bodies and attributes, metric data-point attributes, and
585
+ resource attributes — including the ingest helpers. It does not modify
586
+ telemetry sent through other exporters.
580
587
 
581
588
  ## Loggers
582
589
 
@@ -639,6 +646,85 @@ The official OpenTelemetry JavaScript SDK has no in-process Profiles provider, p
639
646
  - [OpenTelemetry Profiles public alpha announcement](https://opentelemetry.io/blog/2026/profiles-alpha/)
640
647
  - [OpenTelemetry eBPF profiler](https://github.com/open-telemetry/opentelemetry-ebpf-profiler)
641
648
 
649
+ ## TODO(pcga11): Expanded data capture (http2, gRPC, DB results, messaging)
650
+
651
+ Extension points Foam is not using yet, ordered by how much new data they
652
+ unlock. All payload extraction flows through the same redaction and size
653
+ caps as HTTP network capture.
654
+
655
+ ### Database results — biggest win, zero patching
656
+
657
+ `responseHook` hands Foam the raw driver result (in memory, no extra query)
658
+ before the span ends. Each package has its own payload shape — do not
659
+ assume a shared contract: `pg` passes `{ data }`, `mysql2`
660
+ `{ queryResults }`, `mongoose` `{ response }`, `mongodb` the command
661
+ response, `cassandra-driver` only the first result page, and
662
+ `ioredis`/`redis` (v4+) use positional `(cmdName, cmdArgs, response)`
663
+ args. Also `requestHook` (pg, ioredis, oracledb) for full query/args, and
664
+ `maskStatementHook` (mysql2) to wire SQL text into Foam redaction. No
665
+ hooks: `mysql` v1, `tedious`, `memcached` — spans only. Ship as opt-in:
666
+ results are the most PII-dense payload in the system.
667
+ `enhancedDatabaseReporting` (adds bound parameter values) must likewise
668
+ stay opt-in — redaction cannot key-match values inside SQL text.
669
+
670
+ ### http2 + gRPC — currently fully dark
671
+
672
+ Node ships `http2.client.stream.*` / `http2.server.stream.*` lifecycle
673
+ channels since 24.1 (backported to 22.x), and the request-body channels
674
+ `bodyChunkSent` / `bodySent` since 24.12.0 / 22.22.1 — pin those floors in
675
+ `support.ts`, gated on the Node version (core emits these, unlike undici's
676
+ own channels). The adapter mirrors `src/network-capture/undici.ts` but the
677
+ payloads differ: Node publishes `{ stream, writev, data, encoding }` for
678
+ `bodyChunkSent` and `{ stream }` for `bodySent`, vs undici's
679
+ `{ request, chunk }` — map fields explicitly. No OTel instrumentation
680
+ exists for http2, so Foam creates the spans too. Response bodies have no
681
+ channel and the exposed `ClientHttp2Stream` is live, not replayable:
682
+ collect passively (wrap `push()` as `http.ts` does) without reading the
683
+ stream or switching it into flowing mode, or leave response capture out.
684
+ For gRPC, `instrumentation-grpc` gives spans but zero payload hooks
685
+ (only `metadataToSpanAttributes` — enable it). The http2 body chunks carry
686
+ gRPC framing, not bare protobuf: reassemble the 1-byte compression flag +
687
+ 4-byte big-endian length prefix per message, decompressing flagged
688
+ messages, before any protobuf decode — or skip framing entirely with a
689
+ custom patch of `@grpc/grpc-js` serialization.
690
+
691
+ ### Messaging and cloud payloads — hooks receive the actual message
692
+
693
+ - `kafkajs` `producerHook`/`consumerHook` receive `{ topic, message }`
694
+ (value, key, headers); `amqplib` `publishHook`/`consumeHook`
695
+ (+confirm/end variants) receive the message `content` plus
696
+ routing/options metadata — shapes differ per hook.
697
+ - `socket.io` `emitHook`/`onHook`: event payloads (covers the socket.io
698
+ slice of WebSocket traffic).
699
+ - `aws-sdk` `preRequestHook`/`responseHook`/`exceptionHook` (the latter
700
+ gets `(span, requestInfo, err)`): normalized request/response for every
701
+ AWS call. SDK v3 only.
702
+
703
+ ### Crash and shutdown capture — data we currently lose
704
+
705
+ Errors outside any span (timer callbacks, unhandled rejections, startup)
706
+ are invisible, and `beforeExit` — Foam's only lifecycle hook — does not
707
+ fire on crashes or signals, so the batch holding the spans that explain a
708
+ crash dies with the process, and every SIGTERM (each deploy) drops the
709
+ final batch window. Use `uncaughtExceptionMonitor` (never plain
710
+ `uncaughtException`, which suppresses the default crash) — but note an
711
+ async flush started there is abandoned when the process terminates, so
712
+ crash delivery needs a synchronous durable handoff (sync write to disk, or
713
+ a helper process that ships it) rather than relying on the async exporter.
714
+ SIGTERM/SIGINT get flush-then-resignal handlers with a bounded flush
715
+ timeout that never swallows the signal or changes the exit code. `warning`
716
+ events ship as logs.
717
+
718
+ ### Smaller / later
719
+
720
+ - `logHook` (bunyan/pino/winston), `graphql` `responseHook`,
721
+ `express`/`koa`/`restify` `requestHook`: enrichment, little new data.
722
+ - Channels: `worker_threads` (detect uninstrumented workers), 24+ native
723
+ `console.*` (could replace patch-based console capture),
724
+ `tracing:module.*` (library-version detection), `child_process`.
725
+ - No hook surface at all: raw `ws` frames (custom patch, frame data model)
726
+ and Prisma (Rust engine; needs app-enabled Prisma OTel tracing).
727
+
642
728
  ## TODO(pcga11): Anthropic instrumentation
643
729
 
644
730
  There is no released official OpenTelemetry JavaScript instrumentation for the Anthropic SDK yet, but one is actively in progress in js-contrib, based on the OpenInference donation from Arize. Once `@opentelemetry/instrumentation-anthropic` is released (and picked up by `auto-instrumentations-node`), bundle it here like the OpenAI and aws-sdk ones. Until then, Anthropic coverage is handled case by case with the FDE (see FDE.md). Track upstream progress:
@@ -1,7 +1,18 @@
1
+ export interface BeforeSendSpanEventEntry {
2
+ name: string;
3
+ attributes?: Record<string, unknown>;
4
+ time?: unknown;
5
+ }
6
+ export interface BeforeSendSpanLink {
7
+ context: Record<string, unknown>;
8
+ attributes?: Record<string, unknown>;
9
+ }
1
10
  interface BeforeSendSpanEvent {
2
11
  readonly type: "span";
3
12
  name: string;
4
13
  attributes: Record<string, unknown>;
14
+ events: BeforeSendSpanEventEntry[];
15
+ links: BeforeSendSpanLink[];
5
16
  }
6
17
  interface BeforeSendLogEvent {
7
18
  readonly type: "log";
@@ -17,8 +17,13 @@ function isEvent(value, expectedType) {
17
17
  if (!isRecord(value) || value.type !== expectedType || !isRecord(value.attributes)) {
18
18
  return false;
19
19
  }
20
- if (expectedType === "span")
21
- return typeof value.name === "string";
20
+ if (expectedType === "span") {
21
+ return (typeof value.name === "string" &&
22
+ Array.isArray(value.events) &&
23
+ value.events.every((entry) => isRecord(entry) && typeof entry.name === "string") &&
24
+ Array.isArray(value.links) &&
25
+ value.links.every((entry) => isRecord(entry) && isRecord(entry.context)));
26
+ }
22
27
  return value.severityText === undefined || typeof value.severityText === "string";
23
28
  }
24
29
  function invokeBeforeSend(hook, event) {
@@ -4,7 +4,7 @@ export declare const FOAM_INGEST_HOST: string;
4
4
  export declare const ATTR_FOAM_INGEST_HOST = "foam.ingest.host";
5
5
  export declare const FOAM_IDENTIFIER_NAME = "[foam-otel]";
6
6
  export declare const FOAM_DISTRO_NAME = "@foam-ai/node";
7
- export declare const FOAM_DISTRO_VERSION = "0.1.0-alpha.5";
7
+ export declare const FOAM_DISTRO_VERSION = "0.1.0-alpha.12";
8
8
  export declare const FOAM_OTLP_TRACES_PATH = "/v1/traces";
9
9
  export declare const FOAM_OTLP_LOGS_PATH = "/v1/logs";
10
10
  export declare const FOAM_OTLP_METRICS_PATH = "/v1/metrics";
package/dist/constants.js CHANGED
@@ -7,7 +7,7 @@ exports.FOAM_INGEST_HOST = new URL(exports.FOAM_ENDPOINT).hostname;
7
7
  exports.ATTR_FOAM_INGEST_HOST = "foam.ingest.host";
8
8
  exports.FOAM_IDENTIFIER_NAME = "[foam-otel]";
9
9
  exports.FOAM_DISTRO_NAME = "@foam-ai/node";
10
- exports.FOAM_DISTRO_VERSION = "0.1.0-alpha.5";
10
+ exports.FOAM_DISTRO_VERSION = "0.1.0-alpha.12";
11
11
  exports.FOAM_OTLP_TRACES_PATH = "/v1/traces";
12
12
  exports.FOAM_OTLP_LOGS_PATH = "/v1/logs";
13
13
  exports.FOAM_OTLP_METRICS_PATH = "/v1/metrics";
@@ -27,7 +27,8 @@ export declare class FoamLogExporter extends OTLPLogExporter {
27
27
  }
28
28
  export declare class FoamMetricExporter extends OTLPMetricExporter {
29
29
  private readonly stamp?;
30
- constructor(token: string, stamp?: Stamp);
30
+ private readonly options;
31
+ constructor(token: string, stamp?: Stamp, options?: FoamExporterOptions);
31
32
  export(resourceMetrics: ResourceMetrics, callback: (result: ExportResult) => void): void;
32
33
  }
33
34
  export {};
package/dist/exporters.js CHANGED
@@ -13,7 +13,15 @@ const redaction_js_1 = require("./redaction.js");
13
13
  function redactionConfig(options) {
14
14
  return options.redaction ?? (0, redaction_js_1.getActiveRedactionConfig)();
15
15
  }
16
- function copySpan(span, name, attributes, events, links, overlay) {
16
+ // Snapshotting attributes here is safe: every SDK processor (batch/simple
17
+ // span, batch log, periodic metric reader) awaits the resource's
18
+ // waitForAsyncAttributes() before calling the exporter, so detector
19
+ // promises are already resolved.
20
+ function redactedResource(resource, overlay, config) {
21
+ const merged = overlay ? resource.merge(overlay) : resource;
22
+ return (0, resources_1.resourceFromAttributes)((0, redaction_js_1.redactAttributesCopy)(merged.attributes, config), { schemaUrl: merged.schemaUrl });
23
+ }
24
+ function copySpan(span, name, attributes, events, links, resource) {
17
25
  return {
18
26
  name,
19
27
  kind: span.kind,
@@ -27,7 +35,7 @@ function copySpan(span, name, attributes, events, links, overlay) {
27
35
  events,
28
36
  duration: span.duration,
29
37
  ended: span.ended,
30
- resource: overlay ? span.resource.merge(overlay) : span.resource,
38
+ resource,
31
39
  instrumentationScope: span.instrumentationScope,
32
40
  droppedAttributesCount: span.droppedAttributesCount,
33
41
  droppedEventsCount: span.droppedEventsCount,
@@ -52,33 +60,44 @@ class FoamTraceExporter extends exporter_trace_otlp_http_1.OTLPTraceExporter {
52
60
  for (const span of spans) {
53
61
  let name = span.name;
54
62
  let sourceAttributes = span.attributes;
63
+ let sourceEvents = span.events ?? [];
64
+ let sourceLinks = span.links ?? [];
55
65
  if (this.options.beforeSend) {
56
66
  const result = (0, before_send_js_1.invokeBeforeSend)(this.options.beforeSend, {
57
67
  type: "span",
58
68
  name,
59
69
  attributes: sourceAttributes,
70
+ events: sourceEvents,
71
+ links: sourceLinks,
60
72
  });
61
73
  if (result === null)
62
74
  continue;
63
75
  if (result?.type === "span") {
64
76
  name = result.name;
65
77
  sourceAttributes = result.attributes;
78
+ // Hook-added events may omit time; default it to the span start so
79
+ // OTLP serialization stays valid.
80
+ sourceEvents = result.events.map((entry) => ({
81
+ time: span.startTime,
82
+ ...entry,
83
+ }));
84
+ sourceLinks = result.links;
66
85
  }
67
86
  }
68
87
  const attributes = (0, redaction_js_1.redactAttributesCopy)(sourceAttributes, config);
69
- const events = (span.events ?? []).map((event) => event.attributes
88
+ const events = sourceEvents.map((event) => event.attributes
70
89
  ? {
71
90
  ...event,
72
91
  attributes: (0, redaction_js_1.redactAttributesCopy)(event.attributes, config),
73
92
  }
74
93
  : event);
75
- const links = (span.links ?? []).map((link) => link.attributes
94
+ const links = sourceLinks.map((link) => link.attributes
76
95
  ? {
77
96
  ...link,
78
97
  attributes: (0, redaction_js_1.redactAttributesCopy)(link.attributes, config),
79
98
  }
80
99
  : link);
81
- output.push(copySpan(span, name, attributes, events, links, overlay));
100
+ output.push(copySpan(span, name, attributes, events, links, redactedResource(span.resource, overlay, config)));
82
101
  }
83
102
  if (output.length === 0) {
84
103
  callback({ code: core_1.ExportResultCode.SUCCESS });
@@ -88,7 +107,7 @@ class FoamTraceExporter extends exporter_trace_otlp_http_1.OTLPTraceExporter {
88
107
  }
89
108
  }
90
109
  exports.FoamTraceExporter = FoamTraceExporter;
91
- function copyLog(record, body, severityText, attributes, overlay) {
110
+ function copyLog(record, body, severityText, attributes, resource) {
92
111
  return {
93
112
  hrTime: record.hrTime,
94
113
  hrTimeObserved: record.hrTimeObserved,
@@ -97,7 +116,7 @@ function copyLog(record, body, severityText, attributes, overlay) {
97
116
  severityNumber: record.severityNumber,
98
117
  body,
99
118
  eventName: record.eventName,
100
- resource: overlay ? record.resource.merge(overlay) : record.resource,
119
+ resource,
101
120
  instrumentationScope: record.instrumentationScope,
102
121
  attributes,
103
122
  droppedAttributesCount: record.droppedAttributesCount,
@@ -146,7 +165,7 @@ class FoamLogExporter extends exporter_logs_otlp_http_1.OTLPLogExporter {
146
165
  }
147
166
  const body = redactBody(sourceBody, config);
148
167
  const attributes = (0, redaction_js_1.redactAttributesCopy)(sourceAttributes, config);
149
- output.push(copyLog(record, body, severityText, attributes, overlay));
168
+ output.push(copyLog(record, body, severityText, attributes, redactedResource(record.resource, overlay, config)));
150
169
  }
151
170
  if (output.length === 0) {
152
171
  callback({ code: core_1.ExportResultCode.SUCCESS });
@@ -158,21 +177,31 @@ class FoamLogExporter extends exporter_logs_otlp_http_1.OTLPLogExporter {
158
177
  exports.FoamLogExporter = FoamLogExporter;
159
178
  class FoamMetricExporter extends exporter_metrics_otlp_http_1.OTLPMetricExporter {
160
179
  stamp;
161
- constructor(token, stamp) {
180
+ options;
181
+ constructor(token, stamp, options = {}) {
162
182
  super({
163
183
  url: `${endpoint_js_1.endpoint}${constants_js_1.FOAM_OTLP_METRICS_PATH}`,
164
184
  headers: { Authorization: `Bearer ${token}` },
165
185
  });
166
186
  this.stamp = stamp;
187
+ this.options = options;
167
188
  }
168
189
  export(resourceMetrics, callback) {
169
- if (!this.stamp) {
170
- super.export(resourceMetrics, callback);
171
- return;
172
- }
190
+ const config = redactionConfig(this.options);
191
+ const overlay = this.stamp ? (0, resources_1.resourceFromAttributes)(this.stamp) : undefined;
173
192
  super.export({
174
193
  ...resourceMetrics,
175
- resource: resourceMetrics.resource.merge((0, resources_1.resourceFromAttributes)(this.stamp)),
194
+ resource: redactedResource(resourceMetrics.resource, overlay, config),
195
+ scopeMetrics: resourceMetrics.scopeMetrics.map((scope) => ({
196
+ ...scope,
197
+ metrics: scope.metrics.map((metric) => ({
198
+ ...metric,
199
+ dataPoints: metric.dataPoints.map((point) => ({
200
+ ...point,
201
+ attributes: (0, redaction_js_1.redactAttributesCopy)(point.attributes, config),
202
+ })),
203
+ })),
204
+ })),
176
205
  }, callback);
177
206
  }
178
207
  }
package/dist/ingest.d.ts CHANGED
@@ -9,4 +9,4 @@ export interface FoamIngestOptions {
9
9
  }
10
10
  export declare function createFoamIngestSpanProcessor(name: string, environment: string, token: string, options?: FoamIngestOptions): SpanProcessor;
11
11
  export declare function createFoamIngestLogRecordProcessor(name: string, environment: string, token: string, options?: FoamIngestOptions): LogRecordProcessor;
12
- export declare function createFoamIngestMetricReader(name: string, environment: string, token: string): PeriodicExportingMetricReader;
12
+ export declare function createFoamIngestMetricReader(name: string, environment: string, token: string, options?: FoamIngestOptions): PeriodicExportingMetricReader;
package/dist/ingest.js CHANGED
@@ -60,10 +60,10 @@ function createFoamIngestLogRecordProcessor(name, environment, token, options =
60
60
  reportIngestSignal(state_js_1.Signals.logs, name, environment, token);
61
61
  return processor;
62
62
  }
63
- function createFoamIngestMetricReader(name, environment, token) {
63
+ function createFoamIngestMetricReader(name, environment, token, options = {}) {
64
64
  const { stamp } = prepareStamp(name, environment, token);
65
65
  const reader = new sdk_metrics_1.PeriodicExportingMetricReader({
66
- exporter: new exporters_js_1.FoamMetricExporter(token, stamp),
66
+ exporter: new exporters_js_1.FoamMetricExporter(token, stamp, prepareOptions(options)),
67
67
  });
68
68
  reportIngestSignal(state_js_1.Signals.metrics, name, environment, token);
69
69
  return reader;
package/dist/init.js CHANGED
@@ -107,7 +107,9 @@ function init(options) {
107
107
  resource,
108
108
  readers: [
109
109
  new sdk_metrics_1.PeriodicExportingMetricReader({
110
- exporter: new exporters_js_1.FoamMetricExporter(options.token),
110
+ exporter: new exporters_js_1.FoamMetricExporter(options.token, undefined, {
111
+ redaction: redactionConfig,
112
+ }),
111
113
  }),
112
114
  ...(options.additionalMetricReaders ?? []),
113
115
  ],
@@ -1,3 +1,4 @@
1
1
  export declare const SENSITIVE_KEYS: readonly ["admin_password", "basic_auth_password", "confirm_password", "connection_password", "current_password", "database_password", "db_pass", "db_password", "ftp_password", "http_password", "keystore_password", "master_password", "mail_password", "mysql_pwd", "new_password", "old_password", "pass", "passcode", "passphrase", "passwd", "password", "password_confirmation", "postgres_password", "pwd", "redis_password", "root_password", "smtp_password", "user_password", "access_token_secret", "activation_token", "access_token", "api_token", "assertion", "auth", "auth_token", "authentication_token", "authorization", "authorization_code", "bearer", "bearer_token", "bot_token", "ci_job_token", "client_assertion", "client_secret", "credential", "credentials", "deploy_token", "email_verification_token", "id_token", "identity_token", "invite_token", "jwt", "jwt_token", "magic_link_token", "oauth_token", "oauth2_token", "oauth_token_secret", "password_reset_token", "personal_access_token", "registration_token", "request_token", "refresh_token", "reset_token", "saml_assertion", "saml_response", "secret_token", "security_token", "service_token", "sso_token", "token", "unsubscribe_token", "verification_token", "access_key", "access_key_id", "api_key", "api_secret", "amqp_url", "apikey", "app_key", "app_secret", "application_key", "application_secret", "aws_access_key_id", "aws_secret_access_key", "consumer_key", "consumer_secret", "broker_url", "connection_string", "connection_uri", "credential_blob", "decryption_key", "database_url", "database_connection_string", "db_url", "dsn", "encryption_key", "gcp_service_account_key", "google_application_credentials", "key", "key_password", "key_store_password", "keystore", "license_key", "mnemonic", "mongodb_uri", "kube_config", "kubeconfig", "pem", "private_key", "privatekey", "proxy_authorization", "redis_url", "redis_uri", "rabbitmq_url", "secret", "secret_access_key", "secret_key", "seed_phrase", "service_account_json", "service_account_key", "shared_secret", "signature", "signing_key", "signing_secret", "ssh_private_key", "smtp_url", "tls_private_key", "wallet_private_key", "wallet_seed", "webhook_secret", "www_authenticate", "_csrf", "_csrf_token", "_session", "_xsrf", "aiohttp_session", "anti_forgery_token", "backup_code", "backup_codes", "connect.sid", "cookie", "csrf", "csrf_token", "csrftoken", "django_session", "hotp", "laravel_session", "mfa", "mfa_code", "otp", "phpsessid", "pin", "recovery_code", "recovery_codes", "remember_token", "session", "session_id", "session_key", "session_token", "sessionid", "set_cookie", "setcookie", "sid", "symfony", "totp", "user_session", "x_csrf_token", "x_csrftoken", "x_xsrf_token", "xsrf", "xsrf_token", "account_number", "bank_account", "card", "card_number", "credit_card", "credit_card_number", "cvc", "cvv", "date_of_birth", "dob", "driver_license", "drivers_license", "iban", "ip_address", "national_id", "passport_number", "remote_addr", "routing_number", "sort_code", "ssn", "tax_id", "x_forwarded_for", "x_real_ip", "algolia_admin_api_key", "anthropic_api_key", "artifactory_api_key", "airtable_api_key", "azure_api_key", "buildkite_agent_token", "circle_token", "cohere_api_key", "cloudflare_api_token", "consul_http_token", "datadog_api_key", "datadog_app_key", "discord_bot_token", "docker_password", "docker_config_json", "digitalocean_token", "firebase_service_account", "gemini_api_key", "github_token", "gitlab_token", "google_api_key", "groq_api_key", "honeycomb_api_key", "huggingface_token", "heroku_api_key", "jfrog_access_token", "linear_api_key", "mapbox_access_token", "new_relic_license_key", "nomad_token", "notion_token", "npm_token", "openai_api_key", "pagerduty_routing_key", "pypi_token", "sentry_auth_token", "sentry_dsn", "sendgrid_api_key", "shopify_access_token", "shopify_api_secret", "slack_app_token", "slack_bot_token", "slack_signing_secret", "stripe_secret_key", "stripe_webhook_secret", "supabase_service_role_key", "telegram_bot_token", "terraform_cloud_token", "twilio_auth_token", "vault_token", "vercel_oidc_token", "x_api_key", "x_auth_token"];
2
+ export declare const SENSITIVE_KEY_SEGMENTS: readonly ["apikey", "assertion", "auth", "bearer", "cookie", "cookies", "credential", "credentials", "csrf", "cvc", "cvv", "dsn", "hotp", "iban", "jwt", "mfa", "mnemonic", "otp", "passcode", "passphrase", "passport", "passwd", "password", "passwords", "pin", "pwd", "secret", "secrets", "session", "signature", "ssn", "token", "totp", "xsrf", "access_key", "account_number", "api_key", "app_key", "application_key", "card_number", "connection_string", "decryption_key", "encryption_key", "license_key", "master_key", "private_key", "routing_number", "secret_key", "seed_phrase", "service_account", "signing_key", "ssh_key"];
2
3
  export declare const SENSITIVE_HTTP_HEADERS: readonly ["authentication_info", "authorization", "cf_access_authenticated_user_email", "cf_access_jwt_assertion", "cookie", "grpcgateway_authorization", "impersonate_group", "impersonate_user", "jenkins_crumb", "job_token", "ocp_apim_subscription_key", "private_token", "proxy_authenticate", "proxy_authentication_info", "proxy_authorization", "set_cookie", "stripe_signature", "www_authenticate", "x_access_token", "x_algolia_api_key", "x_amz_credential", "x_amz_security_token", "x_amz_signature", "x_amzn_oidc_accesstoken", "x_amzn_oidc_data", "x_amzn_oidc_identity", "x_api_key", "x_asana_request_token", "x_aws_ec2_metadata_token", "x_auth_request_access_token", "x_auth_request_email", "x_auth_request_groups", "x_auth_request_preferred_username", "x_auth_request_user", "x_auth_token", "x_box_signature_primary", "x_box_signature_secondary", "x_cf_access_jwt_assertion", "x_csrf_token", "x_circle_token", "x_consul_token", "x_credential_identifier", "x_datadog_api_key", "x_discord_signature_ed25519", "x_docusign_signature_1", "x_dropbox_signature", "x_elastic_client_authentication", "x_firebase_appcheck", "x_forwarded_access_token", "x_forwarded_email", "x_forwarded_for", "x_forwarded_user", "x_functions_key", "x_github_token", "x_gitlab_token", "x_goog_api_key", "x_goog_firebase_installations_auth", "x_goog_iap_jwt_assertion", "x_hub_signature", "x_hub_signature_256", "x_honeycomb_api_key", "x_honeycomb_team", "x_intercom_hmac", "x_jwt_assertion", "x_linear_signature", "x_mailgun_signature", "x_master_key", "x_meili_api_key", "x_ms_client_principal", "x_ms_token_aad_access_token", "x_ms_token_aad_id_token", "x_ms_token_aad_refresh_token", "x_nf_client_connection_ip", "x_nomad_token", "x_npm_token", "x_original_authorization", "x_real_ip", "x_remote_email", "x_remote_groups", "x_remote_user", "x_pagerduty_signature", "x_parse_rest_api_key", "x_sendgrid_event_webhook_signature", "x_session_token", "x_shopify_hmac_sha256", "x_signature", "x_signature_ed25519", "x_slack_signature", "x_sonarqube_passcode", "x_squarespace_hmacsha256_signature", "x_twilio_signature", "x_typesense_api_key", "x_userinfo", "x_vault_token", "x_vercel_oidc_token", "x_webhook_signature", "x_webhook_secret", "x_wix_webhook_signature", "x_xsrf_token", "x_zendesk_webhook_signature"];
3
4
  export declare const SENSITIVE_QUERY_KEYS: readonly ["access_token", "api_key", "api_secret", "api_token", "apikey", "assertion", "auth", "auth_token", "authorization", "authorization_code", "bearer_token", "client_assertion", "client_secret", "code", "code_verifier", "credential", "hmac", "id_token", "invite_token", "jwt", "key", "magic_link_token", "oauth_token_secret", "otp", "password_reset_token", "password", "policy", "pin", "refresh_token", "reset_token", "saml_response", "secret", "secret_key", "session", "session_id", "session_token", "sig", "signature", "token", "token_secret", "unsubscribe_token", "verification_token", "x_amz_credential", "x_amz_security_token", "x_amz_signature", "x_goog_credential", "x_goog_signature"];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SENSITIVE_QUERY_KEYS = exports.SENSITIVE_HTTP_HEADERS = exports.SENSITIVE_KEYS = void 0;
3
+ exports.SENSITIVE_QUERY_KEYS = exports.SENSITIVE_HTTP_HEADERS = exports.SENSITIVE_KEY_SEGMENTS = exports.SENSITIVE_KEYS = void 0;
4
4
  const PASSWORD_KEYS = [
5
5
  "admin_password",
6
6
  "basic_auth_password",
@@ -268,6 +268,65 @@ exports.SENSITIVE_KEYS = [
268
268
  ...PERSONAL_AND_FINANCIAL_KEYS,
269
269
  ...PROVIDER_KEYS,
270
270
  ];
271
+ // Matched as whole underscore-separated segments of the normalized key, so
272
+ // any name *containing* one of these is caught without enumerating every
273
+ // variant: "legacy_api_key_2" matches "api_key", "stripeToken" matches
274
+ // "token", while "tokenizer" and "secretary" match nothing. Terms that would
275
+ // over-match as bare segments (e.g. "key", "code") appear only in compounds.
276
+ exports.SENSITIVE_KEY_SEGMENTS = [
277
+ "apikey",
278
+ "assertion",
279
+ "auth",
280
+ "bearer",
281
+ "cookie",
282
+ "cookies",
283
+ "credential",
284
+ "credentials",
285
+ "csrf",
286
+ "cvc",
287
+ "cvv",
288
+ "dsn",
289
+ "hotp",
290
+ "iban",
291
+ "jwt",
292
+ "mfa",
293
+ "mnemonic",
294
+ "otp",
295
+ "passcode",
296
+ "passphrase",
297
+ "passport",
298
+ "passwd",
299
+ "password",
300
+ "passwords",
301
+ "pin",
302
+ "pwd",
303
+ "secret",
304
+ "secrets",
305
+ "session",
306
+ "signature",
307
+ "ssn",
308
+ "token",
309
+ "totp",
310
+ "xsrf",
311
+ "access_key",
312
+ "account_number",
313
+ "api_key",
314
+ "app_key",
315
+ "application_key",
316
+ "card_number",
317
+ "connection_string",
318
+ "decryption_key",
319
+ "encryption_key",
320
+ "license_key",
321
+ "master_key",
322
+ "private_key",
323
+ "routing_number",
324
+ "secret_key",
325
+ "seed_phrase",
326
+ "service_account",
327
+ "signing_key",
328
+ "ssh_key",
329
+ ];
271
330
  exports.SENSITIVE_HTTP_HEADERS = [
272
331
  "authentication_info",
273
332
  "authorization",
package/dist/redaction.js CHANGED
@@ -21,9 +21,10 @@ const EMPTY_CONFIG = {
21
21
  secretKeys: new Set(),
22
22
  piiKeys: new Set(),
23
23
  };
24
- const FLOOR_KEYS = new Set(redaction_keys_js_1.SENSITIVE_KEYS);
25
- const HTTP_HEADERS = new Set(redaction_keys_js_1.SENSITIVE_HTTP_HEADERS);
26
- const QUERY_KEYS = new Set(redaction_keys_js_1.SENSITIVE_QUERY_KEYS);
24
+ // Lists are normalized like lookups so entries such as "connect.sid" match.
25
+ const FLOOR_KEYS = new Set(redaction_keys_js_1.SENSITIVE_KEYS.map(normalizeKey));
26
+ const HTTP_HEADERS = new Set(redaction_keys_js_1.SENSITIVE_HTTP_HEADERS.map(normalizeKey));
27
+ const QUERY_KEYS = new Set(redaction_keys_js_1.SENSITIVE_QUERY_KEYS.map(normalizeKey));
27
28
  const HEADER_PREFIXES = ["http.request.header.", "http.response.header."];
28
29
  const BARE_QUERY_KEYS = new Set(["url.query"]);
29
30
  const BODY_KEYS = new Set([
@@ -44,22 +45,32 @@ function normalizeKey(key) {
44
45
  .trim()
45
46
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
46
47
  .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
47
- .replace(/-/g, "_")
48
+ .replace(/[-.]/g, "_")
48
49
  .toLowerCase();
49
50
  }
51
+ const FLOOR_SEGMENTS = redaction_keys_js_1.SENSITIVE_KEY_SEGMENTS.map((segment) => `_${segment}_`);
52
+ // Underscore padding makes containment whole-segment: "_my_api_key_2_"
53
+ // contains "_api_key_", while "_monkey_" and "_authorized_" contain nothing.
54
+ function hasFloorSegment(normalized) {
55
+ const padded = `_${normalized}_`;
56
+ return FLOOR_SEGMENTS.some((segment) => padded.includes(segment));
57
+ }
58
+ function isFloorName(normalized) {
59
+ return FLOOR_KEYS.has(normalized) || hasFloorSegment(normalized);
60
+ }
50
61
  function isFloorKey(key) {
51
- return FLOOR_KEYS.has(normalizeKey(key));
62
+ return isFloorName(normalizeKey(key));
52
63
  }
53
64
  function isSensitiveHeader(key) {
54
65
  return HTTP_HEADERS.has(normalizeKey(key));
55
66
  }
56
67
  function isSensitiveQueryKey(key) {
57
68
  const normalized = normalizeKey(key);
58
- return FLOOR_KEYS.has(normalized) || QUERY_KEYS.has(normalized);
69
+ return isFloorName(normalized) || QUERY_KEYS.has(normalized);
59
70
  }
60
71
  function keyMatch(key, config = activeConfig) {
61
72
  const normalized = normalizeKey(key);
62
- if (FLOOR_KEYS.has(normalized))
73
+ if (isFloorName(normalized))
63
74
  return "floor";
64
75
  if (config.piiKeys.has(normalized))
65
76
  return "pii";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node",
3
- "version": "0.1.0-alpha.10",
3
+ "version": "0.1.0-alpha.12",
4
4
  "description": "Foam JavaScript Node.js SDK",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {