@nekuda/webmcp-sdk 0.7.0-dev.19.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 CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  One change, made twice: a page load costs the ingest edge one request instead of
6
6
  four. The wire schema stays at `2` and every event still arrives — the two savings
7
- are a header and an envelope, not a sample. **A minor, not a patch**, because the
8
- 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
9
9
  `batch` is quarantined as `unknown_event` by any transform older than the one that
10
10
  ships with it. The events an install emits, and the fields on them, are byte for
11
11
  byte 0.6.0's.
@@ -13,10 +13,23 @@ byte 0.6.0's.
13
13
  ### Telemetry beacons without a preflight
14
14
 
15
15
  - The telemetry beacon is sent as `text/plain` instead of `application/json`. The
16
- body is unchanged; the header makes an anonymous beacon a CORS simple request, so
17
- the browser stops sending an `OPTIONS` preflight before every event. Keyed beacons
18
- still carry `x-api-key` and still preflight. The edge has mapped `text/plain` to
19
- the same template since ADR-0014, so no server change is needed.
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.
20
33
 
21
34
  ### One beacon per page load
22
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 `x-api-key` too,
60
- * which only adds tenant attribution an unkeyed page sends anonymously to the
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
@@ -180,16 +180,25 @@ function sendToCollect(event, config, scope = globalThis) {
180
180
  } catch {}
181
181
  }
182
182
  var TELEMETRY_CONTENT_TYPE = "text/plain";
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
+ }
183
193
  function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
184
194
  try {
185
195
  const json = JSON.stringify(event);
186
196
  if (json === undefined)
187
197
  return;
188
- const url = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
198
+ const base = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
189
199
  const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
190
200
  const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
191
- if (authenticated)
192
- headers["x-api-key"] = apiKey;
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.19.1";
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) {
@@ -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 is also sent as `x-api-key` on that channel's
37
- * beacons, which only attributes them to this tenant.
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
  /**
@@ -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
- * `x-api-key`, which only adds tenant attribution. Opt out with `telemetry: false`
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 with one `x-api-key`, and two tenants sharing an origin must never travel
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
@@ -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. Auth travels as the `x-api-key`
9
- * **header** (never a query param or body field): the deployed API Gateway
10
- * authorizer's identity source is that header, so a request without it is
11
- * rejected 401 before the authorizer runs, and keeping the key out of the URL
12
- * keeps it out of referrer logs and out of the canonical `payload` column. The
13
- * event JSON is the bare POST body; `org_id` is server-injected from the key.
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 and the
21
- * same `x-api-key` header *when the page configured a key*, on a distinct
22
- * `/v1/telemetry` path that accepts the request either way. That channel runs on
23
- * pages that never key the authenticated one, so a keyless send is the normal
24
- * case, not a failure the backend attributes those by CORS `Origin` instead.
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 `x-api-key` for tenant
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;
@@ -58,9 +63,11 @@ export interface CollectConfig {
58
63
  export declare function sendToCollect(event: TrackingEvent, config: CollectConfig, scope?: object): void;
59
64
  /** CORS-safelisted, so an anonymous beacon needs no preflight — see sendTelemetry. */
60
65
  export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
66
+ /** Query parameter carrying the publishable key; a header would force a preflight (see sendTelemetry). */
67
+ export declare const TELEMETRY_KEY_PARAM = "key";
61
68
  /**
62
69
  * POST a usage-telemetry event to the telemetry endpoint via `fetch(keepalive)`.
63
- * The path is the same whether or not `apiKey` is given: the header only *adds*
70
+ * The path is the same whether or not `apiKey` is given: the key only *adds*
64
71
  * tenant attribution to an event the backend would accept anyway, so a page that
65
72
  * never configures the authenticated channel keeps sending, and the backend falls
66
73
  * back to the CORS `Origin` header. Never throws: any failure is swallowed and a
@@ -69,8 +76,8 @@ export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
69
76
  * `apiKey` is `unknown` because it originates in caller-supplied options that no
70
77
  * compiler checked; anything that is not a non-blank string sends unauthenticated
71
78
  * rather than putting `"undefined"` (or a hostile object's `toString`) on the
72
- * wire. Note that adding the header makes the request non-simple under CORS, so
73
- * an authenticated beacon costs a preflight the anonymous one does not.
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.
74
81
  *
75
82
  * The body is sent as `text/plain`, not `application/json`, and that is a cost
76
83
  * decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
@@ -79,8 +86,17 @@ export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
79
86
  * per event at the edge; at 2026-09 volume that preflight was roughly half of the
80
87
  * seven million requests a day the collect API billed. The edge maps `text/plain`
81
88
  * to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
82
- * on the wire but the header. The keyed beacon keeps `x-api-key`, which forces a
83
- * preflight regardless of content type; that path is a small fraction of traffic.
89
+ * on the wire but the content type.
90
+ *
91
+ * The publishable key travels as the `key` query parameter, not as `x-api-key`. A
92
+ * custom header is what makes a request non-simple, so a keyed beacon with the header
93
+ * paid the preflight whatever its content type — and every hosted-snippet install IS
94
+ * keyed, because the manifest hands it a default publishable key (ADR-0020). Measured
95
+ * the morning after the content-type change, those keyed preflights were still ~200k
96
+ * requests per two hours on the telemetry route. The key is public by design (it sits
97
+ * in the page's HTML), the authorizer reads the query parameter and the header alike,
98
+ * and the WAF redacts the query string from its logs. `x-api-key` stays accepted for
99
+ * older bundles.
84
100
  *
85
101
  * A `401` on this path means the key that WAS sent resolved to nothing; the beacon
86
102
  * itself was still recorded, anonymously. That is reported through
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nekuda/webmcp-sdk",
3
- "version": "0.7.0-dev.19.1",
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",