@sfranalytics/mcp 0.6.2 → 0.6.4
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 +67 -50
- package/dist/analytics/logger.d.ts +3 -0
- package/dist/analytics/logger.js +39 -0
- package/dist/analytics/logger.js.map +1 -0
- package/dist/analytics/requestContext.d.ts +21 -0
- package/dist/analytics/requestContext.js +39 -0
- package/dist/analytics/requestContext.js.map +1 -0
- package/dist/analytics/sanitize.d.ts +1 -0
- package/dist/analytics/sanitize.js +78 -0
- package/dist/analytics/sanitize.js.map +1 -0
- package/dist/http.js +260 -41
- package/dist/http.js.map +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/landing.d.ts +3 -0
- package/dist/landing.js +904 -0
- package/dist/landing.js.map +1 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +12 -4
- package/dist/server.js.map +1 -1
- package/dist/services/httpClient.d.ts +22 -0
- package/dist/services/httpClient.js +147 -26
- package/dist/services/httpClient.js.map +1 -1
- package/dist/tools/formatters.js +33 -1
- package/dist/tools/formatters.js.map +1 -1
- package/dist/tools/health.js +84 -7
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/plr/borrowerContacts.js +47 -10
- package/dist/tools/plr/borrowerContacts.js.map +1 -1
- package/dist/tools/plr/borrowerProfile.js +11 -5
- package/dist/tools/plr/borrowerProfile.js.map +1 -1
- package/dist/tools/plr/loansNearby.js +100 -84
- package/dist/tools/plr/loansNearby.js.map +1 -1
- package/dist/tools/plr/transactionHistory.js +67 -50
- package/dist/tools/plr/transactionHistory.js.map +1 -1
- package/dist/tools/registerToolSafe.d.ts +5 -1
- package/dist/tools/registerToolSafe.js +110 -4
- package/dist/tools/registerToolSafe.js.map +1 -1
- package/dist/tools/sfr/bestBuyers.js +58 -41
- package/dist/tools/sfr/bestBuyers.js.map +1 -1
- package/dist/tools/sfr/buyerProfile.js +6 -3
- package/dist/tools/sfr/buyerProfile.js.map +1 -1
- package/dist/tools/sfr/getProperty.js +147 -124
- package/dist/tools/sfr/getProperty.js.map +1 -1
- package/dist/tools/sfr/propertyBatch.js +3 -1
- package/dist/tools/sfr/propertyBatch.js.map +1 -1
- package/dist/tools/sfr/propertyComps.js +51 -34
- package/dist/tools/sfr/propertyComps.js.map +1 -1
- package/dist/tools/sfr/propertyTransactions.js +48 -31
- package/dist/tools/sfr/propertyTransactions.js.map +1 -1
- package/dist/tools/sfr/rentalComparables.js +83 -66
- package/dist/tools/sfr/rentalComparables.js.map +1 -1
- package/dist/tools/sfr/rentalMarketAnalysis.js +5 -2
- package/dist/tools/sfr/rentalMarketAnalysis.js.map +1 -1
- package/dist/tools/sfr/rentalStats.js +19 -3
- package/dist/tools/sfr/rentalStats.js.map +1 -1
- package/dist/tools/sfr/searchProperties.js +3 -1
- package/dist/tools/sfr/searchProperties.js.map +1 -1
- package/dist/tools/sfr/topBuyers.js +4 -0
- package/dist/tools/sfr/topBuyers.js.map +1 -1
- package/dist/tools/sfr/zipDetail.js +83 -72
- package/dist/tools/sfr/zipDetail.js.map +1 -1
- package/dist/tools/sfr/zipFinder.js +24 -15
- package/dist/tools/sfr/zipFinder.js.map +1 -1
- package/dist/tools/welcome.d.ts +3 -0
- package/dist/tools/welcome.js +58 -0
- package/dist/tools/welcome.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,68 +1,54 @@
|
|
|
1
1
|
# @sfranalytics/mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [SFR Analytics](https://www.sfranalytics.com) — single-family
|
|
3
|
+
MCP server for [SFR Analytics](https://www.sfranalytics.com) — single-family residential property data, buyer intelligence, and private lending insights for AI assistants.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
### Claude Code
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
claude mcp add
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
claude mcp add sfranalytics --transport http https://mcp.sfranalytics.com/mcp \
|
|
11
|
+
-H "SFR-Api-Token: YOUR_SFR_KEY" \
|
|
12
|
+
-H "PLR-Api-Token: YOUR_PLR_KEY"
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Provide at least one token header (`SFR-Api-Token` or `PLR-Api-Token`).
|
|
16
|
+
|
|
15
17
|
### Claude Desktop
|
|
16
18
|
|
|
19
|
+
Claude Desktop currently uses stdio transport, so use `npx`:
|
|
20
|
+
|
|
17
21
|
Add to your `claude_desktop_config.json`:
|
|
18
22
|
|
|
19
23
|
```json
|
|
20
24
|
{
|
|
21
25
|
"mcpServers": {
|
|
22
|
-
"
|
|
26
|
+
"sfranalytics": {
|
|
23
27
|
"command": "npx",
|
|
24
28
|
"args": ["-y", "@sfranalytics/mcp"],
|
|
25
29
|
"env": {
|
|
26
|
-
"SFR_API_TOKEN": "
|
|
27
|
-
"PLR_API_TOKEN": "
|
|
30
|
+
"SFR_API_TOKEN": "YOUR_SFR_KEY",
|
|
31
|
+
"PLR_API_TOKEN": "YOUR_PLR_KEY"
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
###
|
|
38
|
+
### Other MCP Clients
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
Use the hosted streamable HTTP endpoint:
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
42
|
+
- Endpoint: `https://mcp.sfranalytics.com/mcp`
|
|
43
|
+
- Headers: `SFR-Api-Token` and/or `PLR-Api-Token`
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
### Verify Setup
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
claude mcp add sfra-http --transport http http://localhost:3000/mcp \
|
|
46
|
-
--header "SFR-Api-Token: your-sfr-key" \
|
|
47
|
-
--header "PLR-Api-Token: your-plr-key"
|
|
48
|
-
```
|
|
47
|
+
After connecting, ask Claude:
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
> Check my SFR Analytics connection
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
claude mcp add sfra-http --transport http https://mcp.sfranalytics.com/mcp \
|
|
54
|
-
--header "SFR-Api-Token: your-sfr-key" \
|
|
55
|
-
--header "PLR-Api-Token: your-plr-key"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Notes:
|
|
59
|
-
|
|
60
|
-
- Provide at least one token header (`SFR-Api-Token` or `PLR-Api-Token`).
|
|
61
|
-
- `MCP_ALLOWED_ORIGINS` controls allowed `Origin` headers.
|
|
62
|
-
- `MCP_ALLOWED_HOSTS` controls allowed `Host` headers (recommended for public deployments).
|
|
63
|
-
- `MCP_STRICT_TOKEN_VALIDATION=true` optionally validates token(s) on each MCP request via upstream auth probes.
|
|
64
|
-
- In strict mode, explicit auth failures return `401`; temporary probe outages return `503`.
|
|
65
|
-
- Raw HTTP clients should send `Accept: application/json, text/event-stream`.
|
|
51
|
+
This calls `sfra_health` and confirms your configured API access.
|
|
66
52
|
|
|
67
53
|
## API Keys
|
|
68
54
|
|
|
@@ -102,7 +88,47 @@ Get your API keys at [sfranalytics.com](https://www.sfranalytics.com).
|
|
|
102
88
|
|------|-------------|
|
|
103
89
|
| `sfra_health` | Connectivity check for configured APIs |
|
|
104
90
|
|
|
105
|
-
##
|
|
91
|
+
## Example Prompts
|
|
92
|
+
|
|
93
|
+
The server includes built-in prompts for common workflows:
|
|
94
|
+
|
|
95
|
+
- **market-screen** — Find top zip codes by investment criteria
|
|
96
|
+
- **buyer-research** — Research a property investor's portfolio and strategy
|
|
97
|
+
- **property-analysis** — Evaluate a property for SFR investment
|
|
98
|
+
- **lending-overview** — Private lending market overview
|
|
99
|
+
- **borrower-outreach** — Build a targeted borrower outreach list
|
|
100
|
+
|
|
101
|
+
Prompts are scoped to your configured APIs.
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
<summary>Self-Hosted / Local Development</summary>
|
|
105
|
+
|
|
106
|
+
### Requirements
|
|
107
|
+
|
|
108
|
+
- Node.js >= 18.0.0
|
|
109
|
+
- An MCP-compatible client (Claude Code, Claude Desktop, etc.)
|
|
110
|
+
|
|
111
|
+
### Local stdio (npx)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx -y @sfranalytics/mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Local HTTP server
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run dev:http
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Connect from Claude Code:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
claude mcp add sfranalytics-local --transport http http://localhost:3000/mcp \
|
|
127
|
+
-H "SFR-Api-Token: YOUR_SFR_KEY" \
|
|
128
|
+
-H "PLR-Api-Token: YOUR_PLR_KEY"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Configuration
|
|
106
132
|
|
|
107
133
|
| Variable | Required | Default | Description |
|
|
108
134
|
|----------|----------|---------|-------------|
|
|
@@ -118,24 +144,15 @@ Get your API keys at [sfranalytics.com](https://www.sfranalytics.com).
|
|
|
118
144
|
| `MCP_STRICT_TOKEN_VALIDATION` | No | `false` | If `true`, validate provided HTTP token(s) on each request using upstream auth probes |
|
|
119
145
|
| `MCP_SFR_TOKEN_PROBE_ADDRESS` | No | `123 Main St, Phoenix, AZ 85004` | Address used for SFR strict-token auth probe |
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## Example Prompts
|
|
124
|
-
|
|
125
|
-
The server includes built-in prompts for common workflows:
|
|
126
|
-
|
|
127
|
-
- **market-screen** — Find top zip codes by investment criteria
|
|
128
|
-
- **buyer-research** — Research a property investor's portfolio and strategy
|
|
129
|
-
- **property-analysis** — Evaluate a property for SFR investment
|
|
130
|
-
- **lending-overview** — Private lending market overview
|
|
131
|
-
- **borrower-outreach** — Build a targeted borrower outreach list
|
|
132
|
-
|
|
133
|
-
Prompts are scoped to your configured APIs.
|
|
147
|
+
Notes:
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
- At least one token is required.
|
|
150
|
+
- `MCP_ALLOWED_ORIGINS` controls allowed `Origin` headers.
|
|
151
|
+
- `MCP_ALLOWED_HOSTS` controls allowed `Host` headers.
|
|
152
|
+
- In strict mode, explicit auth failures return `401`; temporary probe outages return `503`.
|
|
153
|
+
- Raw HTTP clients should send `Accept: application/json, text/event-stream`.
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
- An MCP-compatible client (Claude Code, Claude Desktop, etc.)
|
|
155
|
+
</details>
|
|
139
156
|
|
|
140
157
|
## Support
|
|
141
158
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function parseBoolean(input, defaultValue) {
|
|
2
|
+
if (!input)
|
|
3
|
+
return defaultValue;
|
|
4
|
+
const normalized = input.trim().toLowerCase();
|
|
5
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
6
|
+
return true;
|
|
7
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
8
|
+
return false;
|
|
9
|
+
return defaultValue;
|
|
10
|
+
}
|
|
11
|
+
function parseSampleRate(input) {
|
|
12
|
+
if (!input)
|
|
13
|
+
return 1;
|
|
14
|
+
const parsed = Number(input);
|
|
15
|
+
if (!Number.isFinite(parsed))
|
|
16
|
+
return 1;
|
|
17
|
+
if (parsed <= 0)
|
|
18
|
+
return 0;
|
|
19
|
+
if (parsed >= 1)
|
|
20
|
+
return 1;
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
const ANALYTICS_ENABLED = parseBoolean(process.env.MCP_ANALYTICS_ENABLED, true);
|
|
24
|
+
const SAMPLE_RATE = parseSampleRate(process.env.MCP_ANALYTICS_SAMPLE_RATE);
|
|
25
|
+
export function logEvent(event) {
|
|
26
|
+
if (!ANALYTICS_ENABLED)
|
|
27
|
+
return;
|
|
28
|
+
if (SAMPLE_RATE <= 0)
|
|
29
|
+
return;
|
|
30
|
+
if (SAMPLE_RATE < 1 && Math.random() > SAMPLE_RATE)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Logging must never break tool execution.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/analytics/logger.ts"],"names":[],"mappings":"AAEA,SAAS,YAAY,CAAC,KAAyB,EAAE,YAAqB;IACpE,IAAI,CAAC,KAAK;QAAE,OAAO,YAAY,CAAC;IAChC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IACnE,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,KAAyB;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC;IACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,CAAC;IACvC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1B,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,iBAAiB,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;AAChF,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AAE3E,MAAM,UAAU,QAAQ,CAAC,KAAqB;IAC5C,IAAI,CAAC,iBAAiB;QAAE,OAAO;IAC/B,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO;IAC7B,IAAI,WAAW,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,GAAG,WAAW;QAAE,OAAO;IAE3D,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type RuntimeMode = "hosted_http" | "stdio";
|
|
2
|
+
export type RequestContext = {
|
|
3
|
+
requestId: string | null;
|
|
4
|
+
tokenHash: string | null;
|
|
5
|
+
tokenHashSfr: string | null;
|
|
6
|
+
tokenHashPlr: string | null;
|
|
7
|
+
mode: RuntimeMode;
|
|
8
|
+
clientIp?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Compute a token hash for analytics correlation.
|
|
12
|
+
* If MCP_TOKEN_HASH_PEPPER is configured, use HMAC-SHA256.
|
|
13
|
+
* Otherwise use plain SHA-256 for backwards compatibility.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeTokenHash(token: string | null | undefined): string | null;
|
|
16
|
+
export declare function buildTokenHashes(input: {
|
|
17
|
+
sfrToken?: string | null;
|
|
18
|
+
plrToken?: string | null;
|
|
19
|
+
}): Pick<RequestContext, "tokenHash" | "tokenHashSfr" | "tokenHashPlr">;
|
|
20
|
+
export declare function runWithRequestContext<T>(ctx: RequestContext, cb: () => T): T;
|
|
21
|
+
export declare function getCtx(): RequestContext | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createHash, createHmac } from "node:crypto";
|
|
3
|
+
const requestContext = new AsyncLocalStorage();
|
|
4
|
+
function normalizeSecret(input) {
|
|
5
|
+
if (!input)
|
|
6
|
+
return null;
|
|
7
|
+
const trimmed = input.trim();
|
|
8
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compute a token hash for analytics correlation.
|
|
12
|
+
* If MCP_TOKEN_HASH_PEPPER is configured, use HMAC-SHA256.
|
|
13
|
+
* Otherwise use plain SHA-256 for backwards compatibility.
|
|
14
|
+
*/
|
|
15
|
+
export function computeTokenHash(token) {
|
|
16
|
+
const normalizedToken = normalizeSecret(token);
|
|
17
|
+
if (!normalizedToken)
|
|
18
|
+
return null;
|
|
19
|
+
const pepper = normalizeSecret(process.env.MCP_TOKEN_HASH_PEPPER);
|
|
20
|
+
if (pepper) {
|
|
21
|
+
const digest = createHmac("sha256", pepper).update(normalizedToken).digest("hex");
|
|
22
|
+
return `hmac-sha256:${digest}`;
|
|
23
|
+
}
|
|
24
|
+
const digest = createHash("sha256").update(normalizedToken).digest("hex");
|
|
25
|
+
return `sha256:${digest}`;
|
|
26
|
+
}
|
|
27
|
+
export function buildTokenHashes(input) {
|
|
28
|
+
const tokenHashSfr = computeTokenHash(input.sfrToken);
|
|
29
|
+
const tokenHashPlr = computeTokenHash(input.plrToken);
|
|
30
|
+
const tokenHash = tokenHashSfr ?? tokenHashPlr;
|
|
31
|
+
return { tokenHash, tokenHashSfr, tokenHashPlr };
|
|
32
|
+
}
|
|
33
|
+
export function runWithRequestContext(ctx, cb) {
|
|
34
|
+
return requestContext.run(ctx, cb);
|
|
35
|
+
}
|
|
36
|
+
export function getCtx() {
|
|
37
|
+
return requestContext.getStore();
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=requestContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestContext.js","sourceRoot":"","sources":["../../src/analytics/requestContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAarD,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAE/D,SAAS,eAAe,CAAC,KAAgC;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAgC;IAC/D,MAAM,eAAe,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,CAAC,eAAe;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAClE,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClF,OAAO,eAAe,MAAM,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1E,OAAO,UAAU,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAGhC;IACC,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,YAAY,IAAI,YAAY,CAAC;IAC/C,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAI,GAAmB,EAAE,EAAW;IACvE,OAAO,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,cAAc,CAAC,QAAQ,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sanitizeArgs(args: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const MAX_STRING_CHARS = 256;
|
|
2
|
+
const MAX_ARRAY_ITEMS = 20;
|
|
3
|
+
const MAX_OBJECT_KEYS = 100;
|
|
4
|
+
const MAX_DEPTH = 8;
|
|
5
|
+
const REDACTED = "[redacted]";
|
|
6
|
+
const TRUNCATED = "[truncated]";
|
|
7
|
+
const CIRCULAR = "[circular]";
|
|
8
|
+
const KEY_DENYLIST = /ssn|social|address|street|dob|birth|email|phone|token|secret|password|authorization|api[-_]?key/i;
|
|
9
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;
|
|
10
|
+
const PHONE_PATTERN = /^\+?[0-9()\-\s.]{10,}$/;
|
|
11
|
+
const TOKENISH_PATTERN = /^(?:[a-f0-9]{32,}|[A-Za-z0-9+/_\-]{32,})$/;
|
|
12
|
+
function truncateString(value) {
|
|
13
|
+
if (value.length <= MAX_STRING_CHARS)
|
|
14
|
+
return value;
|
|
15
|
+
return value.slice(0, MAX_STRING_CHARS) + TRUNCATED;
|
|
16
|
+
}
|
|
17
|
+
function looksSensitiveValue(value) {
|
|
18
|
+
const normalized = value.trim();
|
|
19
|
+
if (!normalized)
|
|
20
|
+
return false;
|
|
21
|
+
return EMAIL_PATTERN.test(normalized) || PHONE_PATTERN.test(normalized) || TOKENISH_PATTERN.test(normalized);
|
|
22
|
+
}
|
|
23
|
+
function sanitizeValue(value, keyName, depth, seen) {
|
|
24
|
+
if (depth > MAX_DEPTH)
|
|
25
|
+
return TRUNCATED;
|
|
26
|
+
if (value == null)
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
if ((keyName && KEY_DENYLIST.test(keyName)) || looksSensitiveValue(value))
|
|
30
|
+
return REDACTED;
|
|
31
|
+
return truncateString(value);
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
return Number.isFinite(value) ? value : String(value);
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "boolean")
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value === "bigint")
|
|
39
|
+
return value.toString();
|
|
40
|
+
if (value instanceof Date)
|
|
41
|
+
return value.toISOString();
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
const capped = value.slice(0, MAX_ARRAY_ITEMS);
|
|
44
|
+
const sanitized = capped.map((entry) => sanitizeValue(entry, keyName, depth + 1, seen));
|
|
45
|
+
if (value.length > MAX_ARRAY_ITEMS)
|
|
46
|
+
sanitized.push(TRUNCATED);
|
|
47
|
+
return sanitized;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "object") {
|
|
50
|
+
const obj = value;
|
|
51
|
+
if (seen.has(obj))
|
|
52
|
+
return CIRCULAR;
|
|
53
|
+
seen.add(obj);
|
|
54
|
+
const out = {};
|
|
55
|
+
const entries = Object.entries(obj).slice(0, MAX_OBJECT_KEYS);
|
|
56
|
+
for (const [k, v] of entries) {
|
|
57
|
+
if (KEY_DENYLIST.test(k)) {
|
|
58
|
+
out[k] = REDACTED;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out[k] = sanitizeValue(v, k, depth + 1, seen);
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(obj).length > MAX_OBJECT_KEYS) {
|
|
64
|
+
out._truncatedKeys = true;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return truncateString(String(value));
|
|
69
|
+
}
|
|
70
|
+
export function sanitizeArgs(args) {
|
|
71
|
+
const seen = new WeakSet();
|
|
72
|
+
const sanitized = sanitizeValue(args, null, 0, seen);
|
|
73
|
+
if (sanitized && typeof sanitized === "object" && !Array.isArray(sanitized)) {
|
|
74
|
+
return sanitized;
|
|
75
|
+
}
|
|
76
|
+
return { value: sanitized };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/analytics/sanitize.ts"],"names":[],"mappings":"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"}
|