within-sdk 1.0.0 → 1.0.1

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/LICENSE CHANGED
@@ -1 +1 @@
1
- MIT
1
+ MIT
package/README.md CHANGED
@@ -1,77 +1,211 @@
1
- # Within SDK
2
-
3
- MCP analytics and workflow-intelligence SDK for Within.
4
-
5
- ```ts
6
- import { track } from "within-sdk";
7
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
-
9
- const server = new McpServer({ name: "acme-mcp", version: "1.0.0" });
10
-
11
- track(server, "acme", {
12
- apiKey: process.env.WITHIN_SDK_API_KEY!,
13
- identify: async (request) => {
14
- const user = await lookupUser(request);
15
- return {
16
- userId: user.internalCustomerId,
17
- userData: {
18
- plan: user.plan,
19
- segment: user.segment,
20
- },
21
- };
22
- },
23
- });
24
- ```
25
-
26
- `track(server, vendorSlug, options)` instruments MCP initialize, tools/list, and
27
- tools/call activity. The default instrumentation includes:
28
-
29
- - tracing is enabled
30
- - tool-call `context` capture is enabled
31
- - the `get_more_tools` feedback tool is enabled
32
-
33
- Events are posted only to Within. `options.apiBaseUrl` overrides the default
34
- ingestion host; otherwise the SDK checks `WITHIN_SDK_API_URL`, then
35
- `WITHIN_SDK_INGEST_BASE_URL`.
36
-
37
- ## Privacy
38
-
39
- `identify()` keeps the existing public shape, while Within hashes `userId` locally
40
- with the vendor slug before enqueueing or posting telemetry. Raw `userId` and
41
- `userName` are not sent. `userData`, parameters, responses, tags, and properties
42
- are stripped of direct identity fields and redacted/truncated before they reach
43
- Within or vendor-configured exporters.
44
-
45
- Within ingestion also runs server-side Presidio redaction before SDK
46
- activity is stored. The first deployment is model-free: it uses Presidio
47
- rule/checksum/pattern recognizers for values such as emails, phones, SSNs,
48
- card-like values, URLs, IPs, bearer tokens, API keys, and secrets. It does not
49
- claim guaranteed detection of arbitrary names or locations without a later
50
- model-backed recognizer profile.
51
-
52
- ## Conversion Reporting
53
-
54
- Report confirmed lead-to-subscriber conversion from trusted server-side code:
55
-
56
- ```ts
57
- import { reportConversion } from "within-sdk";
58
-
59
- await reportConversion({
60
- vendorSlug: "acme",
61
- apiKey: process.env.WITHIN_SDK_API_KEY!,
62
- }, {
63
- subject: user.internalCustomerId,
64
- convertedAt: new Date(),
65
- plan: {
66
- id: "pro",
67
- name: "Pro",
68
- interval: "month",
69
- },
70
- metadata: {
71
- source: "checkout_webhook",
72
- },
73
- });
74
- ```
75
-
76
- Conversion reporting hashes the subject locally and sends only the hash to
77
- Within.
1
+ # Within SDK
2
+
3
+ Instrument Model Context Protocol (MCP) servers with privacy-conscious workflow
4
+ analytics. Within captures MCP activity, builds a catalog of available tools,
5
+ groups pseudonymous user journeys, identifies qualified opportunities, and links
6
+ confirmed conversions back to the workflows that produced them.
7
+
8
+ [Documentation](https://apidocs.getwith.in) ·
9
+ [Quickstart](https://apidocs.getwith.in/quickstart) ·
10
+ [API Reference](https://apidocs.getwith.in/sdk/api-reference) ·
11
+ [Get an API key](https://getwith.in/dashboard/signup)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install within-sdk @modelcontextprotocol/sdk
17
+ ```
18
+
19
+ The SDK requires Node.js 20 or later and an MCP server using
20
+ `@modelcontextprotocol/sdk` 1.11 or later.
21
+
22
+ ## Environment
23
+
24
+ Configure the vendor slug created during onboarding and its SDK API key in the
25
+ server process that runs your MCP server:
26
+
27
+ ```bash
28
+ WITHIN_VENDOR_SLUG=acme
29
+ WITHIN_SDK_API_KEY=within_sk_xxx
30
+ ```
31
+
32
+ Keep the API key in server-side environment or secret storage. Do not expose it
33
+ in browser code or commit it to source control.
34
+
35
+ ## Quickstart
36
+
37
+ Register your MCP tools first, then call `track()` on the server instance used
38
+ by the running process.
39
+
40
+ ```ts
41
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
42
+ import { track } from "within-sdk";
43
+
44
+ const server = new McpServer({ name: "acme-mcp", version: "1.0.0" });
45
+
46
+ server.tool("search_companies", SearchCompaniesSchema, async (args) => {
47
+ return searchCompanies(args);
48
+ });
49
+
50
+ track(server, process.env.WITHIN_VENDOR_SLUG!, {
51
+ apiKey: process.env.WITHIN_SDK_API_KEY!,
52
+ identify: async (_request, extra) => {
53
+ const user = await lookupUserFromSession(extra?.sessionId);
54
+ if (!user) return null;
55
+
56
+ return {
57
+ userId: user.internalCustomerId,
58
+ userData: {
59
+ plan: user.plan,
60
+ segment: user.segment,
61
+ },
62
+ };
63
+ },
64
+ });
65
+
66
+ export { server };
67
+ ```
68
+
69
+ Use a stable, opaque vendor-local ID for `userId` rather than an email address,
70
+ name, or organization domain. The SDK hashes it locally with the vendor slug
71
+ before telemetry leaves your process.
72
+
73
+ By default, `track()` instruments MCP initialization, `tools/list`, and
74
+ `tools/call` activity. It also enables tool-call context capture and registers
75
+ the `get_more_tools` feedback tool without changing the results returned to MCP
76
+ clients.
77
+
78
+ ## Public APIs
79
+
80
+ ### `track()`
81
+
82
+ ```ts
83
+ track(server, vendorSlug, options?): server
84
+ ```
85
+
86
+ Instruments a high-level `McpServer` or compatible low-level MCP server and
87
+ returns the same server instance. Call it once for each server instance after
88
+ registering tools.
89
+
90
+ ### `publishCustomEvent()`
91
+
92
+ Publish a vendor-defined workflow event associated with a tracked server:
93
+
94
+ ```ts
95
+ import { publishCustomEvent } from "within-sdk";
96
+
97
+ await publishCustomEvent(server, "acme", {
98
+ resourceName: "checkout_started",
99
+ parameters: { plan: "pro" },
100
+ message: "User started checkout after an MCP workflow",
101
+ tags: { channel: "mcp" },
102
+ });
103
+ ```
104
+
105
+ When passed a tracked server, the function reuses its session, API key, and
106
+ ingestion URL. You can also pass an MCP session ID string and provide `apiKey`
107
+ in the event data.
108
+
109
+ ### `reportConversion()`
110
+
111
+ Report a confirmed lead-to-subscriber conversion from trusted server-side
112
+ checkout, webhook, or account-upgrade code:
113
+
114
+ ```ts
115
+ import { reportConversion } from "within-sdk";
116
+
117
+ const result = await reportConversion(
118
+ {
119
+ vendorSlug: process.env.WITHIN_VENDOR_SLUG!,
120
+ apiKey: process.env.WITHIN_SDK_API_KEY!,
121
+ },
122
+ {
123
+ subject: user.internalCustomerId,
124
+ convertedAt: new Date(),
125
+ plan: {
126
+ id: "pro",
127
+ name: "Pro",
128
+ interval: "month",
129
+ },
130
+ metadata: {
131
+ source: "checkout_webhook",
132
+ },
133
+ },
134
+ );
135
+ ```
136
+
137
+ Only `subject` is required. Use the same opaque subject used by `identify()`;
138
+ the SDK hashes it locally before sending the report. Repeated reports for the
139
+ same subject on the same UTC day return `inserted: false`.
140
+
141
+ ### `hashWithinSubject()`
142
+
143
+ Generate the same vendor-scoped subject hash used by identification and
144
+ conversion reporting. This is useful for tests and local verification.
145
+
146
+ ```ts
147
+ import { hashWithinSubject } from "within-sdk";
148
+
149
+ const subjectHash = hashWithinSubject("acme", user.internalCustomerId);
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ `track(server, vendorSlug, options)` accepts these commonly used options:
155
+
156
+ | Option | Purpose |
157
+ | --- | --- |
158
+ | `apiKey` | Within SDK API key. Falls back to `WITHIN_SDK_API_KEY`, then `WITHIN_SDK_INGEST_KEY`. |
159
+ | `apiBaseUrl` | Override the Within ingestion URL. Most integrations should use the default. |
160
+ | `identify` | Resolve a stable vendor-local subject and optional redacted traits for the current request. |
161
+ | `enableTracing` | Capture supported MCP activity. Defaults to `true`. |
162
+ | `enableToolCallContext` | Add and capture the tool-call `context` parameter. Defaults to `true`. |
163
+ | `customContextDescription` | Replace the default description shown for the injected `context` parameter. |
164
+ | `enableReportMissing` | Register the `get_more_tools` feedback tool. Defaults to `true`. |
165
+ | `eventTags` | Add validated string tags to captured activity. |
166
+ | `eventProperties` | Add custom properties to captured activity. |
167
+ | `redactSensitiveInformation` | Apply an additional vendor-provided redaction function. |
168
+ | `privacy` | Configure field/event byte limits and additional keys to redact. |
169
+ | `exporters` | Send redacted telemetry to optional vendor-configured exporters. |
170
+
171
+ `apiBaseUrl` falls back to `WITHIN_SDK_API_URL`, then
172
+ `WITHIN_SDK_INGEST_BASE_URL`, and finally the default Within ingestion service.
173
+ See the [API Reference](https://apidocs.getwith.in/sdk/api-reference) for the
174
+ complete option and result types.
175
+
176
+ ## Privacy and redaction
177
+
178
+ - `userId` and conversion subjects are hashed locally with the vendor slug.
179
+ - Raw `userId` and `userName` values are not sent to Within.
180
+ - Identity-like fields in user data, parameters, responses, tags, properties,
181
+ and conversion metadata are removed or redacted before sending.
182
+ - Configurable field and event limits truncate oversized payloads.
183
+ - Within ingestion applies an additional server-side Presidio redaction pass
184
+ for recognized values such as emails, phone numbers, SSNs, card-like values,
185
+ URLs, IPs, bearer tokens, API keys, and secrets.
186
+
187
+ Pattern-based redaction cannot guarantee detection of every possible name,
188
+ location, or sensitive value. Send only data needed for workflow analytics and
189
+ use opaque identifiers whenever possible.
190
+
191
+ ## Documentation
192
+
193
+ - [Within SDK documentation](https://apidocs.getwith.in)
194
+ - [Quickstart](https://apidocs.getwith.in/quickstart)
195
+ - [API Reference](https://apidocs.getwith.in/sdk/api-reference)
196
+ - [Configuration](https://apidocs.getwith.in/sdk/configuration)
197
+ - [Privacy and redaction](https://apidocs.getwith.in/sdk/privacy-redaction)
198
+ - [Troubleshooting](https://apidocs.getwith.in/troubleshooting)
199
+
200
+ ## Onboarding: Sign up and get an API key
201
+
202
+ 1. [Create a Within dashboard account](https://getwith.in/dashboard/signup)
203
+ using your work email, vendor name, and vendor slug.
204
+ 2. Enter the confirmation code sent to your email, then sign in to the Within
205
+ dashboard.
206
+ 3. Open **Settings**, select your vendor, and choose **Generate** under
207
+ **SDK API Key**.
208
+ 4. Copy the newly displayed key and store it securely. The dashboard does not
209
+ retain the plaintext key for later display.
210
+ 5. Set the key as `WITHIN_SDK_API_KEY`, set your registered slug as
211
+ `WITHIN_VENDOR_SLUG`, and use both values in `track()`.
@@ -1,49 +1,49 @@
1
- "use strict";
2
-
3
- const maxLength = (array, from, to) =>
4
- Math.ceil((array.length * Math.log2(from)) / Math.log2(to));
5
-
6
- function baseConvertIntArray(array, { from, to, fixedLength = null }) {
7
- const length =
8
- fixedLength === null ? maxLength(array, from, to) : fixedLength;
9
- const result = new Array(length);
10
-
11
- // Each iteration prepends the resulting value, so start the offset at the end.
12
- let offset = length;
13
- let input = array;
14
- while (input.length > 0) {
15
- if (offset === 0) {
16
- throw new RangeError(
17
- `Fixed length of ${fixedLength} is too small, expected at least ${maxLength(array, from, to)}`,
18
- );
19
- }
20
-
21
- const quotients = [];
22
- let remainder = 0;
23
-
24
- for (const digit of input) {
25
- const acc = digit + remainder * from;
26
- const q = Math.floor(acc / to);
27
- remainder = acc % to;
28
-
29
- if (quotients.length > 0 || q > 0) {
30
- quotients.push(q);
31
- }
32
- }
33
-
34
- result[--offset] = remainder;
35
- input = quotients;
36
- }
37
-
38
- // Trim leading padding, unless length is fixed.
39
- if (fixedLength === null) {
40
- return offset > 0 ? result.slice(offset) : result;
41
- }
42
-
43
- // Fill in any holes in the result array.
44
- while (offset > 0) {
45
- result[--offset] = 0;
46
- }
47
- return result;
48
- }
49
- export default baseConvertIntArray;
1
+ "use strict";
2
+
3
+ const maxLength = (array, from, to) =>
4
+ Math.ceil((array.length * Math.log2(from)) / Math.log2(to));
5
+
6
+ function baseConvertIntArray(array, { from, to, fixedLength = null }) {
7
+ const length =
8
+ fixedLength === null ? maxLength(array, from, to) : fixedLength;
9
+ const result = new Array(length);
10
+
11
+ // Each iteration prepends the resulting value, so start the offset at the end.
12
+ let offset = length;
13
+ let input = array;
14
+ while (input.length > 0) {
15
+ if (offset === 0) {
16
+ throw new RangeError(
17
+ `Fixed length of ${fixedLength} is too small, expected at least ${maxLength(array, from, to)}`,
18
+ );
19
+ }
20
+
21
+ const quotients = [];
22
+ let remainder = 0;
23
+
24
+ for (const digit of input) {
25
+ const acc = digit + remainder * from;
26
+ const q = Math.floor(acc / to);
27
+ remainder = acc % to;
28
+
29
+ if (quotients.length > 0 || q > 0) {
30
+ quotients.push(q);
31
+ }
32
+ }
33
+
34
+ result[--offset] = remainder;
35
+ input = quotients;
36
+ }
37
+
38
+ // Trim leading padding, unless length is fixed.
39
+ if (fixedLength === null) {
40
+ return offset > 0 ? result.slice(offset) : result;
41
+ }
42
+
43
+ // Fill in any holes in the result array.
44
+ while (offset > 0) {
45
+ result[--offset] = 0;
46
+ }
47
+ return result;
48
+ }
49
+ export default baseConvertIntArray;
@@ -1,24 +1,24 @@
1
- "use strict";
2
- import baseConvertIntArray from "./base-convert-int-array.js";
3
-
4
- const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
5
-
6
- function encode(buffer, fixedLength) {
7
- return baseConvertIntArray(buffer, { from: 256, to: 62, fixedLength })
8
- .map((value) => CHARS[value])
9
- .join("");
10
- }
11
-
12
- function decode(string, fixedLength) {
13
- // Optimization from https://github.com/andrew/base62.js/pull/31.
14
- const input = Array.from(string, (char) => {
15
- const charCode = char.charCodeAt(0);
16
- if (charCode < 58) return charCode - 48;
17
- if (charCode < 91) return charCode - 55;
18
- return charCode - 61;
19
- });
20
- return Buffer.from(
21
- baseConvertIntArray(input, { from: 62, to: 256, fixedLength }),
22
- );
23
- }
24
- export { encode, decode };
1
+ "use strict";
2
+ import baseConvertIntArray from "./base-convert-int-array.js";
3
+
4
+ const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
5
+
6
+ function encode(buffer, fixedLength) {
7
+ return baseConvertIntArray(buffer, { from: 256, to: 62, fixedLength })
8
+ .map((value) => CHARS[value])
9
+ .join("");
10
+ }
11
+
12
+ function decode(string, fixedLength) {
13
+ // Optimization from https://github.com/andrew/base62.js/pull/31.
14
+ const input = Array.from(string, (char) => {
15
+ const charCode = char.charCodeAt(0);
16
+ if (charCode < 58) return charCode - 48;
17
+ if (charCode < 91) return charCode - 55;
18
+ return charCode - 61;
19
+ });
20
+ return Buffer.from(
21
+ baseConvertIntArray(input, { from: 62, to: 256, fixedLength }),
22
+ );
23
+ }
24
+ export { encode, decode };
@@ -1,30 +1,30 @@
1
- /// <reference types="node" />
2
- declare class KSUID {
3
- constructor(buffer: Buffer);
4
- readonly raw: Buffer;
5
- readonly date: Date;
6
- readonly timestamp: number;
7
- readonly payload: Buffer;
8
- readonly string: string;
9
- compare(other: KSUID): number;
10
- equals(other: KSUID): boolean;
11
- toString(): string;
12
- toJSON(): string;
13
- static random(): Promise<KSUID>;
14
- static random(timeInMs: number): Promise<KSUID>;
15
- static random(date: Date): Promise<KSUID>;
16
- static randomSync(): KSUID;
17
- static randomSync(timeInMs: number): KSUID;
18
- static randomSync(date: Date): KSUID;
19
- static fromParts(timeInMs: number, payload: Buffer): KSUID;
20
- static isValid(buffer: Buffer): boolean;
21
- static parse(str: string): KSUID;
22
- static withPrefix(prefix: string): {
23
- random(timeInMs?: number): Promise<string>;
24
- random(date?: Date): Promise<string>;
25
- randomSync(timeInMs?: number): string;
26
- randomSync(date?: Date): string;
27
- fromParts(timeInMs: number, payload: Buffer): string;
28
- };
29
- }
30
- export default KSUID;
1
+ /// <reference types="node" />
2
+ declare class KSUID {
3
+ constructor(buffer: Buffer);
4
+ readonly raw: Buffer;
5
+ readonly date: Date;
6
+ readonly timestamp: number;
7
+ readonly payload: Buffer;
8
+ readonly string: string;
9
+ compare(other: KSUID): number;
10
+ equals(other: KSUID): boolean;
11
+ toString(): string;
12
+ toJSON(): string;
13
+ static random(): Promise<KSUID>;
14
+ static random(timeInMs: number): Promise<KSUID>;
15
+ static random(date: Date): Promise<KSUID>;
16
+ static randomSync(): KSUID;
17
+ static randomSync(timeInMs: number): KSUID;
18
+ static randomSync(date: Date): KSUID;
19
+ static fromParts(timeInMs: number, payload: Buffer): KSUID;
20
+ static isValid(buffer: Buffer): boolean;
21
+ static parse(str: string): KSUID;
22
+ static withPrefix(prefix: string): {
23
+ random(timeInMs?: number): Promise<string>;
24
+ random(date?: Date): Promise<string>;
25
+ randomSync(timeInMs?: number): string;
26
+ randomSync(date?: Date): string;
27
+ fromParts(timeInMs: number, payload: Buffer): string;
28
+ };
29
+ }
30
+ export default KSUID;