@nekuda/webmcp-sdk 0.7.0-dev.20.1 → 0.7.0-dev.21.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/CHANGELOG.md +19 -13
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -2
- package/dist/register.d.ts +3 -2
- package/dist/telemetry.d.ts +4 -3
- package/dist/transport.d.ts +21 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## Unreleased — the telemetry key rides the query string
|
|
4
|
-
|
|
5
|
-
- A keyed telemetry beacon sends its publishable key as the `key` query parameter
|
|
6
|
-
instead of the `x-api-key` header. The header made every keyed beacon a non-simple
|
|
7
|
-
request with an `OPTIONS` preflight, and every hosted-snippet install is keyed. The
|
|
8
|
-
backend reads both; nothing else changes.
|
|
9
|
-
|
|
10
3
|
## 0.7.0 — 2026-09-21
|
|
11
4
|
|
|
12
5
|
One change, made twice: a page load costs the ingest edge one request instead of
|
|
13
6
|
four. The wire schema stays at `2` and every event still arrives — the two savings
|
|
14
|
-
are a
|
|
15
|
-
second of them puts a request shape on the wire that no earlier backend parses: a
|
|
7
|
+
are a request shape and an envelope, not a sample. **A minor, not a patch**, because
|
|
8
|
+
the second of them puts a request shape on the wire that no earlier backend parses: a
|
|
16
9
|
`batch` is quarantined as `unknown_event` by any transform older than the one that
|
|
17
10
|
ships with it. The events an install emits, and the fields on them, are byte for
|
|
18
11
|
byte 0.6.0's.
|
|
@@ -20,10 +13,23 @@ byte 0.6.0's.
|
|
|
20
13
|
### Telemetry beacons without a preflight
|
|
21
14
|
|
|
22
15
|
- The telemetry beacon is sent as `text/plain` instead of `application/json`. The
|
|
23
|
-
body is unchanged; the
|
|
24
|
-
the browser stops sending an `OPTIONS` preflight before every event.
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
body is unchanged; the content type makes an anonymous beacon a CORS simple
|
|
17
|
+
request, so the browser stops sending an `OPTIONS` preflight before every event.
|
|
18
|
+
The edge has mapped `text/plain` to the same template since ADR-0014, so no server
|
|
19
|
+
change is needed.
|
|
20
|
+
- A **keyed** beacon sends its publishable key as the `key` query parameter instead
|
|
21
|
+
of the `x-api-key` header, because the content type alone was not enough: a custom
|
|
22
|
+
request header is what makes a request non-simple, so a keyed beacon kept paying
|
|
23
|
+
the preflight — and every hosted-snippet install is keyed, since the manifest hands
|
|
24
|
+
it a default publishable key. The backend accepts both and the header still wins,
|
|
25
|
+
so older bundles are unaffected.
|
|
26
|
+
- **This reverses 0.2.0's "not `?api_key=`" for this channel only.** That decision is
|
|
27
|
+
unchanged for `/v1/collect`, which carries the *secret* key and whose authorizer
|
|
28
|
+
declares the header as its identity source. It does not carry to telemetry: the key
|
|
29
|
+
there is *publishable* and already sits in the page's HTML, the soft authorizer
|
|
30
|
+
declares no identity source at all, the stage keeps no access logs, and the WAF both
|
|
31
|
+
redacts the query string from its logs and now disables sampled requests, which
|
|
32
|
+
would otherwise have shown the URI in the console.
|
|
27
33
|
|
|
28
34
|
### One beacon per page load
|
|
29
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -56,8 +56,10 @@
|
|
|
56
56
|
* separate `/v1/telemetry` endpoint, fire-and-forget; the two page-level ones may
|
|
57
57
|
* share one request as a `batch` envelope, `tool_call` never does. Arrival order
|
|
58
58
|
* is NOT guaranteed, so consumers join on `sessionId` and never on ordering. A
|
|
59
|
-
* page that configures point 5's `apiKey` has it sent here as `
|
|
60
|
-
*
|
|
59
|
+
* page that configures point 5's `apiKey` has it sent here too, as the `key`
|
|
60
|
+
* query parameter rather than point 5's header (a custom header would make the
|
|
61
|
+
* beacon non-simple under CORS and cost it a preflight); it only adds tenant
|
|
62
|
+
* attribution — an unkeyed page sends anonymously to the
|
|
61
63
|
* same path. It composes with point 5: a call can emit to neither channel, either
|
|
62
64
|
* one, or both. Three page-level levers silence it entirely (no event, no
|
|
63
65
|
* network), because all three are read at emit time:
|
package/dist/index.js
CHANGED
|
@@ -181,6 +181,15 @@ function sendToCollect(event, config, scope = globalThis) {
|
|
|
181
181
|
}
|
|
182
182
|
var TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
183
183
|
var TELEMETRY_KEY_PARAM = "key";
|
|
184
|
+
function withTelemetryKey(base, apiKey) {
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(base);
|
|
187
|
+
url.searchParams.set(TELEMETRY_KEY_PARAM, apiKey);
|
|
188
|
+
return url.toString();
|
|
189
|
+
} catch {
|
|
190
|
+
return `${base}${base.includes("?") ? "&" : "?"}${TELEMETRY_KEY_PARAM}=${encodeURIComponent(apiKey)}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
184
193
|
function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
185
194
|
try {
|
|
186
195
|
const json = JSON.stringify(event);
|
|
@@ -189,7 +198,7 @@ function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
|
189
198
|
const base = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
190
199
|
const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
|
|
191
200
|
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
192
|
-
const url = authenticated ?
|
|
201
|
+
const url = authenticated ? withTelemetryKey(base, apiKey) : base;
|
|
193
202
|
tryFetch(scope, url, headers, json, authenticated ? (response) => {
|
|
194
203
|
if (isUnauthorized(response))
|
|
195
204
|
warnKeyRejected(scope);
|
|
@@ -1105,7 +1114,7 @@ function shapeMetrics(inputSchema) {
|
|
|
1105
1114
|
|
|
1106
1115
|
// src/telemetry.ts
|
|
1107
1116
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1108
|
-
var SDK_VERSION = "0.7.0-dev.
|
|
1117
|
+
var SDK_VERSION = "0.7.0-dev.21.1";
|
|
1109
1118
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1110
1119
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1111
1120
|
function parseSampleRate(raw) {
|
package/dist/register.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ export interface RegisterToolsOptions {
|
|
|
33
33
|
* event before its handler runs and a `tool_call_response` event after.
|
|
34
34
|
* `disabled: true` is this channel's consent gate; it does not narrow the
|
|
35
35
|
* default-on telemetry channel below, which resolves no identity and touches no
|
|
36
|
-
* storage. An `apiKey` set here
|
|
37
|
-
*
|
|
36
|
+
* storage. An `apiKey` set here also rides that channel's beacons — on the URL as
|
|
37
|
+
* `?key=`, not as this channel's header — which only attributes them to this
|
|
38
|
+
* tenant.
|
|
38
39
|
*/
|
|
39
40
|
tracking?: TrackingOptions;
|
|
40
41
|
/**
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Default-on usage telemetry for `@nekuda/webmcp-sdk`. Independent of the opt-in
|
|
3
3
|
* `tracking` channel (`src/tracking.ts`): it needs no `apiKey`, so it reports SDK
|
|
4
4
|
* adoption, browser mix, WebMCP availability, and tool-call reliability from every
|
|
5
|
-
* site the SDK runs on. A page that *does* configure one has it sent as
|
|
6
|
-
*
|
|
5
|
+
* site the SDK runs on. A page that *does* configure one has it sent as the `key`
|
|
6
|
+
* query parameter — never a header, which would cost every keyed beacon a CORS
|
|
7
|
+
* preflight — and it only adds tenant attribution. Opt out with `telemetry: false`
|
|
7
8
|
* on `registerTools`, with `globalThis.__WEBMCP_TELEMETRY__ = false`, or via
|
|
8
9
|
* Global Privacy Control.
|
|
9
10
|
*
|
|
@@ -388,7 +389,7 @@ export interface TelemetrySinks {
|
|
|
388
389
|
* navigation could then lose.
|
|
389
390
|
*
|
|
390
391
|
* The queue is keyed on the destination — endpoint plus key — because a batch is one
|
|
391
|
-
* request
|
|
392
|
+
* request under one key, and two tenants sharing an origin must never travel
|
|
392
393
|
* under each other's key (see {@link batchTelemetrySinks}). The init flush a batch
|
|
393
394
|
* defers (`deferInitEventForBatch`) uses that batch's sinks, so the common case lands
|
|
394
395
|
* in one queue. A `pagehide` or a hidden tab flushes everything at once, through the
|
package/dist/transport.d.ts
CHANGED
|
@@ -5,23 +5,28 @@
|
|
|
5
5
|
* v0.
|
|
6
6
|
*
|
|
7
7
|
* Transport is `fetch(..., { keepalive: true })` — unload-safe and, unlike
|
|
8
|
-
* `sendBeacon`, able to set custom headers.
|
|
9
|
-
* **header** (never a query param or body field): the deployed
|
|
10
|
-
* authorizer's identity source is that header, so a request without
|
|
11
|
-
* rejected 401 before the authorizer runs, and keeping the key out
|
|
12
|
-
* keeps it out of referrer logs and out of the canonical `payload`
|
|
13
|
-
* event JSON is the bare POST body; `org_id` is server-injected from
|
|
8
|
+
* `sendBeacon`, able to set custom headers. On **this** channel auth travels as
|
|
9
|
+
* the `x-api-key` **header** (never a query param or body field): the deployed
|
|
10
|
+
* API Gateway authorizer's identity source is that header, so a request without
|
|
11
|
+
* it is rejected 401 before the authorizer runs, and keeping the secret key out
|
|
12
|
+
* of the URL keeps it out of referrer logs and out of the canonical `payload`
|
|
13
|
+
* column. The event JSON is the bare POST body; `org_id` is server-injected from
|
|
14
|
+
* the key.
|
|
14
15
|
*
|
|
15
16
|
* The scope is injectable for tests (spec.ts pattern). Every browser global is
|
|
16
17
|
* guarded and the whole function is wrapped so telemetry never throws into the
|
|
17
18
|
* caller.
|
|
18
19
|
*
|
|
19
20
|
* `sendTelemetry` is the sibling sender for the default-on usage-telemetry
|
|
20
|
-
* channel (`src/telemetry.ts`): same keepalive/fire-and-forget discipline
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* channel (`src/telemetry.ts`): same keepalive/fire-and-forget discipline, on a
|
|
22
|
+
* distinct `/v1/telemetry` path that accepts the request with or without a key.
|
|
23
|
+
* It does **not** share the header above — it puts the *publishable* key on the
|
|
24
|
+
* URL as `?key=`, because a custom header would cost that channel a CORS
|
|
25
|
+
* preflight it exists to avoid; see {@link sendTelemetry} for the whole reason
|
|
26
|
+
* and for why the exposure that argues against a URL here does not argue against
|
|
27
|
+
* it there. That channel runs on pages that never key the authenticated one, so a
|
|
28
|
+
* keyless send is the normal case, not a failure — the backend attributes those
|
|
29
|
+
* by CORS `Origin` instead.
|
|
25
30
|
*
|
|
26
31
|
* That path has exactly one observable failure signal, and it is a `console.error`,
|
|
27
32
|
* never a throw or a retry: a *keyed* beacon whose key the backend cannot resolve
|
|
@@ -40,7 +45,7 @@ export declare const DEFAULT_COLLECT_ENDPOINT: string;
|
|
|
40
45
|
* Default ingest endpoint for the default-on usage-telemetry channel — a
|
|
41
46
|
* **separate path** from {@link DEFAULT_COLLECT_ENDPOINT} so the backend can route
|
|
42
47
|
* the two channels independently: one authorizer-protected, this one unauthenticated
|
|
43
|
-
* by default and accepting the same request with an optional
|
|
48
|
+
* by default and accepting the same request with an optional `?key=` for tenant
|
|
44
49
|
* attribution. Same per-flavor base host (see {@link INGEST_BASE}).
|
|
45
50
|
*/
|
|
46
51
|
export declare const DEFAULT_TELEMETRY_ENDPOINT: string;
|
|
@@ -62,7 +67,7 @@ export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
|
62
67
|
export declare const TELEMETRY_KEY_PARAM = "key";
|
|
63
68
|
/**
|
|
64
69
|
* POST a usage-telemetry event to the telemetry endpoint via `fetch(keepalive)`.
|
|
65
|
-
* The path is the same whether or not `apiKey` is given: the
|
|
70
|
+
* The path is the same whether or not `apiKey` is given: the key only *adds*
|
|
66
71
|
* tenant attribution to an event the backend would accept anyway, so a page that
|
|
67
72
|
* never configures the authenticated channel keeps sending, and the backend falls
|
|
68
73
|
* back to the CORS `Origin` header. Never throws: any failure is swallowed and a
|
|
@@ -71,8 +76,8 @@ export declare const TELEMETRY_KEY_PARAM = "key";
|
|
|
71
76
|
* `apiKey` is `unknown` because it originates in caller-supplied options that no
|
|
72
77
|
* compiler checked; anything that is not a non-blank string sends unauthenticated
|
|
73
78
|
* rather than putting `"undefined"` (or a hostile object's `toString`) on the
|
|
74
|
-
* wire.
|
|
75
|
-
*
|
|
79
|
+
* wire. A keyed beacon differs from an anonymous one only in its URL: the request
|
|
80
|
+
* stays CORS-simple either way, so neither costs a preflight.
|
|
76
81
|
*
|
|
77
82
|
* The body is sent as `text/plain`, not `application/json`, and that is a cost
|
|
78
83
|
* decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
|
|
@@ -81,7 +86,7 @@ export declare const TELEMETRY_KEY_PARAM = "key";
|
|
|
81
86
|
* per event at the edge; at 2026-09 volume that preflight was roughly half of the
|
|
82
87
|
* seven million requests a day the collect API billed. The edge maps `text/plain`
|
|
83
88
|
* to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
|
|
84
|
-
* on the wire but the
|
|
89
|
+
* on the wire but the content type.
|
|
85
90
|
*
|
|
86
91
|
* The publishable key travels as the `key` query parameter, not as `x-api-key`. A
|
|
87
92
|
* custom header is what makes a request non-simple, so a keyed beacon with the header
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekuda/webmcp-sdk",
|
|
3
|
-
"version": "0.7.0-dev.
|
|
3
|
+
"version": "0.7.0-dev.21.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Phase-1 WebMCP SDK: a thin wrapper over document.modelContext that plugin-generated code targets — defineTool + register/unregister lifecycle. This package pins the plugin↔SDK seam; anonymous tool-call tracking (backend transport via apiKey, OTEL LogRecords via otel) is opt-in through registerTools and default-silent, while anonymous usage telemetry is a separate unauthenticated channel that is on by default (opt out with telemetry: false).",
|
|
6
6
|
"main": "./dist/index.js",
|