within-sdk 1.0.0

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.
Files changed (69) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +77 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.js +243 -0
  5. package/dist/middleware.d.ts +40 -0
  6. package/dist/middleware.js +562 -0
  7. package/dist/modules/compatibility.d.ts +18 -0
  8. package/dist/modules/compatibility.js +99 -0
  9. package/dist/modules/constants.d.ts +12 -0
  10. package/dist/modules/constants.js +12 -0
  11. package/dist/modules/context-parameters.d.ts +13 -0
  12. package/dist/modules/context-parameters.js +74 -0
  13. package/dist/modules/diagnostics.d.ts +7 -0
  14. package/dist/modules/diagnostics.js +12 -0
  15. package/dist/modules/eventQueue.d.ts +32 -0
  16. package/dist/modules/eventQueue.js +234 -0
  17. package/dist/modules/exceptions.d.ts +13 -0
  18. package/dist/modules/exceptions.js +730 -0
  19. package/dist/modules/exporters/datadog.d.ts +17 -0
  20. package/dist/modules/exporters/datadog.js +166 -0
  21. package/dist/modules/exporters/otlp.d.ts +13 -0
  22. package/dist/modules/exporters/otlp.js +140 -0
  23. package/dist/modules/exporters/posthog.d.ts +32 -0
  24. package/dist/modules/exporters/posthog.js +272 -0
  25. package/dist/modules/exporters/sentry.d.ts +27 -0
  26. package/dist/modules/exporters/sentry.js +386 -0
  27. package/dist/modules/exporters/trace-context.d.ts +8 -0
  28. package/dist/modules/exporters/trace-context.js +27 -0
  29. package/dist/modules/index.d.ts +8 -0
  30. package/dist/modules/index.js +8 -0
  31. package/dist/modules/internal.d.ts +33 -0
  32. package/dist/modules/internal.js +195 -0
  33. package/dist/modules/logging.d.ts +2 -0
  34. package/dist/modules/logging.js +74 -0
  35. package/dist/modules/mcp-sdk-compat.d.ts +32 -0
  36. package/dist/modules/mcp-sdk-compat.js +111 -0
  37. package/dist/modules/privacy.d.ts +15 -0
  38. package/dist/modules/privacy.js +179 -0
  39. package/dist/modules/redaction.d.ts +11 -0
  40. package/dist/modules/redaction.js +81 -0
  41. package/dist/modules/sanitization.d.ts +9 -0
  42. package/dist/modules/sanitization.js +111 -0
  43. package/dist/modules/session.d.ts +22 -0
  44. package/dist/modules/session.js +109 -0
  45. package/dist/modules/telemetry.d.ts +8 -0
  46. package/dist/modules/telemetry.js +53 -0
  47. package/dist/modules/tools.d.ts +25 -0
  48. package/dist/modules/tools.js +119 -0
  49. package/dist/modules/tracing.d.ts +4 -0
  50. package/dist/modules/tracing.js +263 -0
  51. package/dist/modules/tracingV2.d.ts +2 -0
  52. package/dist/modules/tracingV2.js +300 -0
  53. package/dist/modules/truncation.d.ts +24 -0
  54. package/dist/modules/truncation.js +303 -0
  55. package/dist/modules/validation.d.ts +6 -0
  56. package/dist/modules/validation.js +51 -0
  57. package/dist/network.d.ts +83 -0
  58. package/dist/network.js +411 -0
  59. package/dist/proxy.d.ts +31 -0
  60. package/dist/proxy.js +158 -0
  61. package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
  62. package/dist/thirdparty/ksuid/base62.js +24 -0
  63. package/dist/thirdparty/ksuid/index.d.ts +30 -0
  64. package/dist/thirdparty/ksuid/index.js +201 -0
  65. package/dist/types.d.ts +197 -0
  66. package/dist/types.js +5 -0
  67. package/dist/validate.d.ts +53 -0
  68. package/dist/validate.js +139 -0
  69. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ MIT
package/README.md ADDED
@@ -0,0 +1,77 @@
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.
@@ -0,0 +1,16 @@
1
+ import { CustomEventData, WithinConversionInput, WithinConversionResult, WithinOptions } from "./types.js";
2
+ import { hashWithinSubject } from "./modules/privacy.js";
3
+ export type { CustomEventData, Exporter, ExporterConfig, MCPServerLike, RedactFunction, UserIdentity, WithinConversionInput, WithinConversionPlan, WithinConversionResult, WithinOptions, } from "./types.js";
4
+ export { hashWithinSubject };
5
+ export interface ReportConversionConfig {
6
+ vendorSlug: string;
7
+ apiKey: string;
8
+ apiBaseUrl?: string;
9
+ fetch?: typeof fetch;
10
+ privacy?: WithinOptions["privacy"];
11
+ }
12
+ declare function track(server: any, vendorSlug: string, options?: WithinOptions): any;
13
+ export declare function publishCustomEvent(serverOrSessionId: any | string, vendorSlug: string, eventData?: CustomEventData): Promise<void>;
14
+ export declare function reportConversion(config: ReportConversionConfig, input: WithinConversionInput): Promise<WithinConversionResult>;
15
+ export type IdentifyFunction = WithinOptions["identify"];
16
+ export { track };
package/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ import { isCompatibleServerType, isHighLevelServer, } from "./modules/compatibility.js";
2
+ import { writeToLog } from "./modules/logging.js";
3
+ import { setupWithinFeedbackTools } from "./modules/tools.js";
4
+ import { setupToolCallTracing } from "./modules/tracing.js";
5
+ import { getSessionInfo, newSessionId, deriveSessionIdFromMCPSession, } from "./modules/session.js";
6
+ import { setServerTrackingData, getServerTrackingData, } from "./modules/internal.js";
7
+ import { setupTracking } from "./modules/tracingV2.js";
8
+ import { TelemetryManager } from "./modules/telemetry.js";
9
+ import { setTelemetryManager, eventQueue, } from "./modules/eventQueue.js";
10
+ import { DEFAULT_WITHIN_INGEST_URL, SDK_VERSION, WITHIN_CUSTOM_EVENT_TYPE, } from "./modules/constants.js";
11
+ import { validateTags } from "./modules/validation.js";
12
+ import { initDiagnostics } from "./modules/diagnostics.js";
13
+ import { hashWithinSubject, redactWithinValue } from "./modules/privacy.js";
14
+ export { hashWithinSubject };
15
+ function track(server, vendorSlug, options = {}) {
16
+ try {
17
+ if (!vendorSlug || typeof vendorSlug !== "string") {
18
+ throw new Error("vendorSlug is required");
19
+ }
20
+ initDiagnostics({
21
+ projectId: vendorSlug,
22
+ disabled: true,
23
+ });
24
+ const validatedServer = isCompatibleServerType(server);
25
+ const isHighLevel = isHighLevelServer(validatedServer);
26
+ const lowLevelServer = (isHighLevel ? validatedServer.server : validatedServer);
27
+ writeToLog(`Within SDK setup started | vendor ${vendorSlug} | server ${isHighLevel ? "high-level" : "low-level"}`);
28
+ const existingData = getServerTrackingData(lowLevelServer);
29
+ if (existingData) {
30
+ writeToLog("[SESSION DEBUG] track() - Server already being tracked, skipping initialization");
31
+ return validatedServer;
32
+ }
33
+ if (options.exporters) {
34
+ const telemetryManager = new TelemetryManager(options.exporters);
35
+ setTelemetryManager(telemetryManager);
36
+ writeToLog(`Initialized telemetry with ${Object.keys(options.exporters).length} exporters`);
37
+ }
38
+ const apiKey = options.apiKey ?? readEnv("WITHIN_SDK_API_KEY") ?? readEnv("WITHIN_SDK_INGEST_KEY");
39
+ if (!apiKey && !options.exporters) {
40
+ writeToLog("Warning: No Within apiKey provided and no exporters configured. Events will not be sent anywhere.");
41
+ }
42
+ eventQueue.configure();
43
+ const sessionInfo = getSessionInfo(lowLevelServer, undefined);
44
+ const withinData = {
45
+ projectId: vendorSlug,
46
+ vendorSlug,
47
+ apiKey,
48
+ ingestBaseUrl: resolveIngestBaseUrl(options.apiBaseUrl),
49
+ fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
50
+ sessionId: newSessionId(),
51
+ lastActivity: new Date(),
52
+ identifiedSessions: new Map(),
53
+ sessionInfo,
54
+ options: {
55
+ ...options,
56
+ apiKey,
57
+ enableReportMissing: options.enableReportMissing ?? true,
58
+ enableTracing: options.enableTracing ?? true,
59
+ enableToolCallContext: options.enableToolCallContext ?? true,
60
+ customContextDescription: options.customContextDescription,
61
+ identify: options.identify,
62
+ redactSensitiveInformation: options.redactSensitiveInformation,
63
+ eventTags: options.eventTags,
64
+ eventProperties: options.eventProperties,
65
+ },
66
+ sessionSource: "within",
67
+ };
68
+ setServerTrackingData(lowLevelServer, withinData);
69
+ if (isHighLevel) {
70
+ setupTracking(validatedServer);
71
+ }
72
+ else {
73
+ if (withinData.options.enableReportMissing) {
74
+ try {
75
+ setupWithinFeedbackTools(lowLevelServer);
76
+ }
77
+ catch (error) {
78
+ writeToLog(`Warning: Failed to setup report missing tool - ${error}`);
79
+ }
80
+ }
81
+ if (withinData.options.enableTracing) {
82
+ try {
83
+ setupToolCallTracing(lowLevelServer);
84
+ }
85
+ catch (error) {
86
+ writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);
87
+ }
88
+ }
89
+ }
90
+ const exporterCount = options.exporters
91
+ ? Object.keys(options.exporters).length
92
+ : 0;
93
+ writeToLog(`Within SDK setup complete | vendor ${vendorSlug} | tracing=${withinData.options.enableTracing} context=${withinData.options.enableToolCallContext} reportMissing=${withinData.options.enableReportMissing} exporters=${exporterCount}`);
94
+ return validatedServer;
95
+ }
96
+ catch (error) {
97
+ writeToLog(`Warning: Failed to track server - ${error}`);
98
+ return server;
99
+ }
100
+ }
101
+ export async function publishCustomEvent(serverOrSessionId, vendorSlug, eventData) {
102
+ if (!vendorSlug) {
103
+ throw new Error("vendorSlug is required for publishCustomEvent");
104
+ }
105
+ let sessionId;
106
+ let data;
107
+ const isServer = typeof serverOrSessionId === "object" && serverOrSessionId !== null;
108
+ if (isServer) {
109
+ const lowLevelServer = serverOrSessionId.server
110
+ ? serverOrSessionId.server
111
+ : serverOrSessionId;
112
+ data = getServerTrackingData(lowLevelServer);
113
+ if (!data) {
114
+ throw new Error("Server is not tracked. Please call track() first or provide a session ID string with eventData.apiKey.");
115
+ }
116
+ sessionId = data.sessionId;
117
+ }
118
+ else if (typeof serverOrSessionId === "string") {
119
+ sessionId = deriveSessionIdFromMCPSession(serverOrSessionId, vendorSlug);
120
+ }
121
+ else {
122
+ throw new Error("First parameter must be either an MCP server object or a session ID string");
123
+ }
124
+ const event = {
125
+ sessionId,
126
+ projectId: vendorSlug,
127
+ vendorSlug: data?.vendorSlug ?? vendorSlug,
128
+ apiKey: data?.apiKey ?? eventData?.apiKey,
129
+ ingestBaseUrl: data?.ingestBaseUrl ?? resolveIngestBaseUrl(eventData?.apiBaseUrl),
130
+ fetchImpl: data?.fetchImpl ?? eventData?.fetch ?? globalThis.fetch.bind(globalThis),
131
+ eventType: WITHIN_CUSTOM_EVENT_TYPE,
132
+ timestamp: new Date(),
133
+ resourceName: eventData?.resourceName,
134
+ parameters: eventData?.parameters,
135
+ response: eventData?.response,
136
+ userIntent: eventData?.message,
137
+ duration: eventData?.duration,
138
+ isError: eventData?.isError,
139
+ error: eventData?.error,
140
+ };
141
+ if (eventData?.tags) {
142
+ event.tags = validateTags(eventData.tags);
143
+ }
144
+ if (eventData?.properties && Object.keys(eventData.properties).length > 0) {
145
+ event.properties = eventData.properties;
146
+ }
147
+ eventQueue.add(event);
148
+ writeToLog(`Published custom event for session ${sessionId} with type '${WITHIN_CUSTOM_EVENT_TYPE}'`);
149
+ }
150
+ export async function reportConversion(config, input) {
151
+ const subject = normalizeSubject(input.subject);
152
+ if (!subject) {
153
+ throw new Error("within-sdk conversion subject is required");
154
+ }
155
+ const convertedAt = normalizeConversionTimestamp(input.convertedAt);
156
+ const conversionUtcDate = convertedAt.slice(0, 10);
157
+ const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
158
+ const body = {
159
+ vendor_slug: config.vendorSlug,
160
+ subject_hash: hashWithinSubject(config.vendorSlug, subject),
161
+ converted_at: convertedAt,
162
+ conversion_utc_date: conversionUtcDate,
163
+ plan: normalizeConversionPlan(input.plan),
164
+ metadata: redactWithinValue(input.metadata, config.privacy),
165
+ sdk_version: SDK_VERSION,
166
+ };
167
+ const res = await fetchImpl(`${resolveIngestBaseUrl(config.apiBaseUrl)}/api/sdk/conversions`, {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json",
171
+ Authorization: `Bearer ${config.apiKey}`,
172
+ "x-within-sdk": "within-sdk",
173
+ },
174
+ body: JSON.stringify(body),
175
+ });
176
+ if (!res.ok) {
177
+ throw new Error(`HTTP ${res.status}: ${await safeText(res)}`);
178
+ }
179
+ return await safeJson(res);
180
+ }
181
+ function resolveIngestBaseUrl(explicit) {
182
+ return trimTrailingSlash(explicit
183
+ ?? readEnv("WITHIN_SDK_API_URL")
184
+ ?? readEnv("WITHIN_SDK_INGEST_BASE_URL")
185
+ ?? DEFAULT_WITHIN_INGEST_URL);
186
+ }
187
+ function readEnv(name) {
188
+ try {
189
+ const value = globalThis.process?.env?.[name]?.trim();
190
+ return value || undefined;
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
195
+ }
196
+ function trimTrailingSlash(value) {
197
+ return value.replace(/\/+$/, "");
198
+ }
199
+ function normalizeConversionTimestamp(value) {
200
+ if (!value)
201
+ return new Date().toISOString();
202
+ const date = value instanceof Date ? value : new Date(value);
203
+ if (!Number.isFinite(date.getTime())) {
204
+ throw new Error("within-sdk conversion convertedAt must be a valid date");
205
+ }
206
+ return date.toISOString();
207
+ }
208
+ function normalizeConversionPlan(plan) {
209
+ if (!plan || typeof plan !== "object")
210
+ return undefined;
211
+ const out = {
212
+ id: normalizePlanField(plan.id),
213
+ name: normalizePlanField(plan.name),
214
+ interval: normalizePlanField(plan.interval),
215
+ };
216
+ return out.id || out.name || out.interval ? out : undefined;
217
+ }
218
+ function normalizePlanField(value) {
219
+ return typeof value === "string" && value.trim().length > 0
220
+ ? value.trim().slice(0, 200)
221
+ : undefined;
222
+ }
223
+ function normalizeSubject(subject) {
224
+ const normalized = subject?.trim();
225
+ return normalized ? normalized : null;
226
+ }
227
+ async function safeText(res) {
228
+ try {
229
+ return (await res.text()).slice(0, 300);
230
+ }
231
+ catch {
232
+ return "";
233
+ }
234
+ }
235
+ async function safeJson(res) {
236
+ try {
237
+ return await res.json();
238
+ }
239
+ catch {
240
+ return undefined;
241
+ }
242
+ }
243
+ export { track };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * With.in MCP Auth SDK — sealed request handler.
3
+ *
4
+ * Exposes ONE function, `createWithinHandler`, that returns a handler with:
5
+ * - `mcpHandler(req, res)` — node HTTP handler for the MCP endpoint
6
+ * - `subscribeHandler(req,res)` — node HTTP handler for the /subscribe route
7
+ * - `vendorSlug`, `authServerUrl` — readonly fields for logging
8
+ *
9
+ * Everything else — token validation, tool-call reporting, trial-exhausted
10
+ * gating, subscriber cross-check + auto conversion, transport construction
11
+ * and session caching, network-tool injection, auth propagation into tool
12
+ * handlers — is hidden inside `mcpHandler`. Vendors cannot accidentally
13
+ * skip a step.
14
+ */
15
+ export interface WithinRequestLike {
16
+ url?: string;
17
+ method?: string;
18
+ headers: Record<string, string | string[] | undefined>;
19
+ on(event: 'data', listener: (chunk: any) => void): unknown;
20
+ on(event: 'end', listener: () => void): unknown;
21
+ }
22
+ export interface WithinResponseLike {
23
+ writeHead(status: number, headers?: Record<string, string>): unknown;
24
+ write(chunk: any): unknown;
25
+ end(body?: string): unknown;
26
+ }
27
+ import type { WithinConfig } from './types.js';
28
+ export interface WithinHandler {
29
+ mcpHandler: (req: WithinRequestLike, res: WithinResponseLike) => Promise<void>;
30
+ subscribeHandler: (req: WithinRequestLike, res: WithinResponseLike) => Promise<void>;
31
+ /** Serves RFC 9728 Protected Resource Metadata at the vendor's well-known
32
+ * path. Derives `resource` from the incoming Host + X-Forwarded-Proto so
33
+ * the byte-equality check in §3.3 passes no matter what hostname the
34
+ * vendor runs under. Vendors route GET /.well-known/oauth-protected-resource
35
+ * through this handler instead of hand-rolling their own JSON. */
36
+ prmHandler: (req: WithinRequestLike, res: WithinResponseLike) => void;
37
+ readonly vendorSlug: string;
38
+ readonly authServerUrl: string;
39
+ }
40
+ export declare function createWithinHandler(config: WithinConfig): WithinHandler;