@revturbine/sdk 0.11.12 → 0.11.16

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 (38) hide show
  1. package/CHANGELOG.md +548 -0
  2. package/dist/growthbook.d.ts +1 -1
  3. package/dist/headless.d.ts +10912 -582
  4. package/dist/headless.js +2 -2
  5. package/dist/headless.js.map +1 -1
  6. package/dist/index.d.ts +10952 -584
  7. package/dist/index.js +2 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/optimizely.d.ts +1 -1
  10. package/dist/types/server-node/client.d.ts +211 -3
  11. package/dist/types/server-node/client.d.ts.map +1 -1
  12. package/dist/types/server-node/index.d.ts +15 -3
  13. package/dist/types/server-node/index.d.ts.map +1 -1
  14. package/dist/types/server-node/local-server.d.ts +15 -2
  15. package/dist/types/server-node/local-server.d.ts.map +1 -1
  16. package/dist/types/server-node/types.d.ts +67 -2
  17. package/dist/types/server-node/types.d.ts.map +1 -1
  18. package/dist/types/web-sdk/config-artifact.d.ts +7 -2
  19. package/dist/types/web-sdk/config-artifact.d.ts.map +1 -1
  20. package/dist/types/web-sdk/controllers.d.ts +16 -0
  21. package/dist/types/web-sdk/controllers.d.ts.map +1 -1
  22. package/dist/types/web-sdk/customer-side.d.ts +179 -31
  23. package/dist/types/web-sdk/customer-side.d.ts.map +1 -1
  24. package/dist/types/web-sdk/headless.d.ts +2 -0
  25. package/dist/types/web-sdk/headless.d.ts.map +1 -1
  26. package/dist/types/web-sdk/local-builtin-segments.d.ts +46 -0
  27. package/dist/types/web-sdk/local-builtin-segments.d.ts.map +1 -0
  28. package/dist/types/web-sdk/react/RevTurbineProvider.d.ts.map +1 -1
  29. package/dist/types/web-sdk/react/RevTurbineRoute.d.ts +45 -0
  30. package/dist/types/web-sdk/react/RevTurbineRoute.d.ts.map +1 -0
  31. package/dist/types/web-sdk/react/index.d.ts +1 -0
  32. package/dist/types/web-sdk/react/index.d.ts.map +1 -1
  33. package/dist/types/web-sdk/react/usePlacement.d.ts.map +1 -1
  34. package/dist/types/web-sdk/slot-route.d.ts +83 -0
  35. package/dist/types/web-sdk/slot-route.d.ts.map +1 -0
  36. package/dist/types/web-sdk/theme/theme-loader.d.ts +12 -2
  37. package/dist/types/web-sdk/theme/theme-loader.d.ts.map +1 -1
  38. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -47,6 +47,554 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.11.16
51
+
52
+ ### Build fix, part two: the npm declaration build resolves `openapi-fetch` too (no API change)
53
+
54
+ **What changed.** 0.11.15 fixed the bundle step, but the public npm publish
55
+ then failed one command later, in the declaration emit
56
+ (`tsc -p tsconfig.build.json`). It reported
57
+ `../server-node/client.ts(29,43): error TS2307: Cannot find module 'openapi-fetch'`,
58
+ for the same reason as before: TypeScript resolves the bare import by walking
59
+ up from `server-node/`, and the public repo installs the package only under
60
+ `web-sdk/`. `web-sdk/tsconfig.build.json` now maps `openapi-fetch` to
61
+ `./node_modules/openapi-fetch`.
62
+
63
+ **npm users:** the 0.11.14 changes first reach npm in 0.11.16. Neither 0.11.14
64
+ nor 0.11.15 was published to npm (both are on GitHub Packages, PyPI and
65
+ crates.io). There is no behavior change across 0.11.14 through 0.11.16.
66
+
67
+ **Landed in:** 0.11.16.
68
+ **Fail-closed in:** n/a (build only).
69
+ **Proving test:** the public repo's `release-npm.yml` build step for
70
+ `v0.11.16`. It was reproduced locally by hiding the root `openapi-fetch` link
71
+ to match the public layout: the declaration emit failed with the same TS2307
72
+ before this change, and tsup, the declaration emit and rollup dts all pass
73
+ after it.
74
+
75
+ ## 0.11.15
76
+
77
+ ### Build fix: the npm package publishes again (no API change)
78
+
79
+ **What changed.** 0.11.14 was published to GitHub Packages, PyPI and
80
+ crates.io, but the public npm publish of `@revturbine/sdk` failed at the bundle
81
+ step: `Could not resolve "openapi-fetch"` from `server-node/client.ts`, which
82
+ the `headless` entry re-exports. `openapi-fetch` has been bundled since BL-0354
83
+ (0.11.14 was the first release to carry it). A bare import resolves by walking
84
+ up from the importing file, so from `server-node/` it never reached
85
+ `web-sdk/node_modules`, which is the only place the public repo installs it.
86
+ `web-sdk/tsup.config.ts` now gives esbuild `web-sdk/node_modules` as a fallback
87
+ `nodePaths` entry, so every file the SDK bundles resolves the SDK's own
88
+ dependencies.
89
+
90
+ **npm users:** everything listed under 0.11.14 below first reaches npm in
91
+ 0.11.15. There is no behavior change between 0.11.14 and 0.11.15. PyPI and
92
+ crates.io get 0.11.15 in lockstep, with no change.
93
+
94
+ **Landed in:** 0.11.15.
95
+ **Fail-closed in:** n/a (build only).
96
+ **Proving test:** the public repo's `release-npm.yml` bundle step (tsup +
97
+ declarations) for `v0.11.15`. Locally, `web-sdk/tsup.config.ts` builds the
98
+ bundle, and an esbuild probe confirms `nodePaths` resolves a bare import from a
99
+ sibling directory that has no `node_modules` of its own.
100
+
101
+ ## 0.11.14
102
+
103
+ This release carries the BL-0335 change below plus every entry that main held
104
+ under "Unreleased" since 0.11.13 (BL-0381, BL-0382, BL-0366, BL-0375, BL-0369,
105
+ BL-0352, BL-0354 and BL-0351).
106
+
107
+ ### `tenantId` is optional on a browser init with a `publicKey`; the key is the sole arbiter of the tenant (BL-0335, D-49)
108
+
109
+ **What changed.** Before, a browser init needed both a `tenantId` and a
110
+ `publicKey`. The public key is bound to exactly one tenant, and the control
111
+ plane already resolved the tenant from the key alone, so the id was redundant
112
+ configuration, and a wrong one was silently ignored. Now:
113
+
114
+ - `RevTurbineInitOptions.tenantId` is **optional**. The minimal hosted init is
115
+ `{ publicKey, endpoint }`.
116
+ - A key-only integration sends **no** `x-tenant-id`. It reads the tenant from
117
+ the key-authenticated Playbook delivery: the signed manifest's `tenant_id`
118
+ and the Playbook's own `tenant_id`. Until the first Playbook loads, the
119
+ tenant is unknown, so `getUserContext().tenant_id` and the event envelope's
120
+ `tenant_id` read `''` and the `/api/track` row omits `tenant_id`. The
121
+ control plane stamps the key's tenant regardless.
122
+ - A **configured** `tenantId` that matches the key's tenant behaves exactly as
123
+ before: it is still sent, and every locally persisted key (anonymous id,
124
+ decision cache, caps, interaction state) is unchanged.
125
+ - A **configured** `tenantId` that does **not** match the key's tenant is no
126
+ longer silently ignored, and it is not refused either (D-49). When the
127
+ key-authenticated delivery names another tenant, the SDK logs one warning
128
+ per init and adopts the key's tenant:
129
+
130
+ ```
131
+ [RevTurbine] tenantId <configured> does not match the key's tenant <key tenant>; using <key tenant>
132
+ ```
133
+
134
+ The signed manifest is activated (before, a manifest whose tenant differed
135
+ from the configured one was refused and the SDK fell back to the legacy
136
+ config fetch). After adoption, `getUserContext().tenant_id`, event envelopes
137
+ and the `x-tenant-id` the SDK still sends all carry the key's tenant. Local
138
+ storage keys stay namespaced by the configured id, so nothing persisted is
139
+ lost.
140
+ - A key-only integration keys local state by a short hash of the public key
141
+ (`revturbine:pk-…:`), so rotating the key starts a fresh local namespace,
142
+ just as changing `tenantId` always did.
143
+ - The theme loader (`fetchThemeOverride`) no longer sends the `'local'`
144
+ placeholder as a tenant header. `ThemeLoaderOptions.tenantId` is optional,
145
+ and the new `storageScope` option keeps the persisted-theme key.
146
+ - `ConfigTargetDefaults.tenantId` is optional. Without it, the artifact's own
147
+ `tenant_id` is used.
148
+ - Init now fails loudly when no tenant can be identified. It throws when
149
+ neither `publicKey` nor `tenantId` is present, and when a `local_only` init
150
+ has no `tenantId` and no `localRuntime.playbook`. With a local Playbook,
151
+ `local_only` still defaults the tenant to `'local'`, and a local Playbook's
152
+ `tenant_id` stays a guard behind the init option (plan 233 REQ-2): there is
153
+ no key there to arbitrate.
154
+
155
+ **Server side (web #927).** The control plane uses the key's tenant
156
+ unconditionally on every public-key lane. A tenant id that IS sent, whether as
157
+ a header or as `tenant_id` in an event or interaction body, is diagnostic only:
158
+ a mismatch is logged server-side and ignored. No request is refused for it.
159
+
160
+ **Landed in:** 0.11.14 (`web-sdk` only; `server-python`/`server-rust` bumped
161
+ in lockstep with no behavior change: the server ports authenticate with the
162
+ server key and build a per-request context with a `tenant_id`, and they have
163
+ no browser `publicKey` init option to relax).
164
+ **Fail-closed in:** 0.11.14. An init with no identifiable tenant throws;
165
+ before, it was a type error only, and JS callers got `undefined`. A mismatching
166
+ configured tenant warns from 0.11.14; before, it was silent.
167
+ **Proving test:**
168
+ - `web-sdk/customer-side-tenant-optional.test.ts`: key-only init on both
169
+ delivery lanes (no tenant header; tenant adopted from the manifest and
170
+ Playbook), configured matching tenant unchanged (header sent, storage keys
171
+ byte-identical), a mismatching configured tenant warned once and the key's
172
+ tenant adopted on both lanes, the key-hash namespace, the init guards, and
173
+ the theme loader.
174
+ - `web-sdk/init-options-exactness.test-d.ts`: a key-only init compiles.
175
+
176
+ ### server-node `setBuiltinDimensions()` — your backend sets built-in dimension values, with an `override` flag (BL-0382, rulings D-46 / D-47)
177
+
178
+ **What changed.** Additive. The customer's backend can now SET built-in
179
+ segment dimension values for a user, not just read the control plane's
180
+ (D-46: "the customer's backend calling revturbine to set the dimension").
181
+
182
+ - **server-node** — `RevTurbineServer.setBuiltinDimensions(userId, dimensions,
183
+ { override })` calls the widened `upsertServerUserContext`
184
+ (`POST /api/sdk/user-contexts`) with `{ user_id, builtin_dimensions,
185
+ override }` and the server key. Settable: `subscription_state`,
186
+ `trial_type`, `billing_health`, `activity_level`, `buyer_role`,
187
+ `email_type`, `region`, `device_type` (typed as
188
+ `ServerBuiltinDimensionsWrite`, each a closed vocabulary; `null` clears a
189
+ dimension). `seat_type` stays `assignSeatType`. It resolves to the stored
190
+ state — `seat_type_handle`, the customer-set `builtin_dimensions`, the
191
+ pinned `overrides`, `updated_at`. A refusal throws the new
192
+ `RevTurbineDimensionWriteError` (`status`, `requestId`, machine `code`,
193
+ typed `reason`: `invalid_request` / `unauthorized` / `forbidden` /
194
+ `unknown_dimension_value` / `request_failed`); it never carries the key.
195
+ - **`override` (D-47).** `false` (default) is an UPDATE: the value is delivered
196
+ until a newer RevTurbine enrichment change supersedes it (last writer wins,
197
+ enrichment included), and writing a pinned dimension without the flag
198
+ unpins it. `true` is an OVERRIDE: the dimension is pinned and enrichment
199
+ never replaces it until you write it again without the flag or clear it.
200
+ The control plane applies this before delivery, so the browser SDK's client
201
+ context and `getBuiltinDimensions()` both return the result; the SDK
202
+ delivery rule and the parity goldens are unchanged.
203
+ - **`assignSeatType(userId, handle, { builtinDimensions, override })`** can
204
+ write dimension values in the same upsert. Its `SeatAssignmentErrorReason`
205
+ gains `unknown_dimension_value` (a 422 with code `UNKNOWN_DIMENSION_VALUE`).
206
+ Its response now also carries `builtin_dimensions` and `overrides`.
207
+ - **Python and Rust get no HTTP client** (unchanged: neither port has a
208
+ server-key transport). A backend on those stacks calls the endpoint itself.
209
+ - Browser input types are untouched: `builtin_dimensions` is never a browser
210
+ write path.
211
+
212
+ The `@revt-eng/*` pins move to `0.1.374`, which widens the operation
213
+ (scaffold #431).
214
+
215
+ **Landed in:** 0.11.14.
216
+ **Fail-closed in:** n/a (additive).
217
+ **Proving test:** `tests/server-node-set-builtin-dimensions.test.ts`,
218
+ `tests/server-node-assign-seat-type.test.ts`.
219
+
220
+ ### Behaviour change: the browser SDK ignores app-set `builtin_dimensions` (BL-0381, ruling D-46)
221
+
222
+ **What changed.** Trust in a built-in dimension value is decided by *party*
223
+ (Kent, D-46): the browser client never asserts one, the customer's server may,
224
+ and RevTurbine's own enrichment is optional. `builtin_dimensions` therefore
225
+ joins `seat_type_handle`, `activity_score` and `activity_score_computed_at`
226
+ as a server-assigned key in the browser SDK.
227
+
228
+ - **Types.** `builtin_dimensions` is omitted from `RevTurbineUserContext`,
229
+ `RevTurbineUpdateInput` and `IdentifyContextInput`, so
230
+ `update({ builtin_dimensions })`, `setUserContext({ builtin_dimensions })`,
231
+ `identify(id, { builtin_dimensions })` and `user: { builtin_dimensions }`
232
+ no longer compile.
233
+ - **Runtime.** Every browser entry point drops the key and warns with the
234
+ server-assigned diagnostic (`update()`, `identify()`, `setUserContext()`,
235
+ the `user` init option). A persisted `local_only` state blob that carries it
236
+ is purged on restore, and a server-action result that echoes it is stripped
237
+ silently. Before this change, 0.11.12 and 0.11.13 merged an app-set value
238
+ through `setUserContext()` and the `user` option. An app could then claim
239
+ any built-in segment (for example `rt.subscription_state.paid`) from the
240
+ browser.
241
+ - **The one browser source** is the authenticated
242
+ `GET /api/sdk/client-context` delivery (`fetchClientContext`, or a
243
+ `clientSession` minter). Its values overlay per leaf exactly as before.
244
+ `getUserContext()` still returns the held value.
245
+ - **`local_only`.** Built-in segment *definitions* are still synthesized for
246
+ a local Playbook (BL-0365), so chips and rules naming `rt.*` segments stay
247
+ valid. Their *values* no longer come from the app: **a browser `local_only`
248
+ app can no longer target built-ins unless a server-provided context
249
+ delivers them.** With no delivery every `rt_*` trait is absent and the
250
+ built-in segments fail closed (PD-4). `rt.registration_state.*` still
251
+ resolves from the identified id alone.
252
+ - **Unchanged: server SDKs.** They are the customer's server. Their overlay
253
+ inputs stay: server-node's `createStaticProviders({ userContext:
254
+ { builtin_dimensions }, serverBuiltinDimensions })`, Python's
255
+ `builtin_dimensions` / `server_builtin_dimensions` and Rust's
256
+ `builtin_dimensions` / `server_builtin_dimensions`. Parity goldens are
257
+ unchanged.
258
+
259
+ This supersedes the part of BL-0352 (below) that made `update()` apply
260
+ `builtin_dimensions`. That change never shipped.
261
+
262
+ **Landed in:** 0.11.14.
263
+ **Fail-closed in:** the same release. The compile-time omission and the
264
+ runtime drop ship together.
265
+ **Proving test:**
266
+ - `web-sdk/update-keys-exhaustiveness.test-d.ts` (`pnpm check:types:exact`):
267
+ `builtin_dimensions` is in the server-assigned set.
268
+ - `web-sdk/identify-guardrails.test.ts`: describe block
269
+ `builtin_dimensions never enters the browser context from the app`.
270
+ - `web-sdk/local-builtin-segments.test.ts`: `chip … with app-set … fails
271
+ closed` (and the delivered cases still match).
272
+ - `web-sdk/customer-side-rt-entitlement-gate.test.ts`: `denies when the app
273
+ sets builtin_dimensions itself via update()`.
274
+ - `web-sdk/customer-side-client-context.test.ts`: `holds exactly the
275
+ delivered value; an app-set value never reaches the context`.
276
+
277
+ ### Server SDKs get the control plane's built-in dimensions: `getBuiltinDimensions()` + a server overlay (BL-0366, plan 279 PD-3)
278
+
279
+ **What changed.** Additive. Before this, a server-side decision never saw the
280
+ control plane's Subscription State, Trial Type, Activity State, Seat Type or
281
+ Buyer Role. The Python and Rust ports stamped `rt_*` traits only from the
282
+ app-supplied `builtin_dimensions`, and the fetch that delivers the server's
283
+ values existed only in the browser SDK's client-context lane.
284
+
285
+ - **server-node** — `RevTurbineServer.getBuiltinDimensions(userId)` calls
286
+ `getServerUserBuiltinDimensions`
287
+ (`GET /api/sdk/user-contexts/{userId}/builtin-dimensions`) through the
288
+ generated typed client with the server key the client already holds. It
289
+ resolves to `{ tenant_id, user_id, builtin_dimensions, resolved_at }`
290
+ (re-exported as `ServerUserBuiltinDimensions`). The tenant comes from the
291
+ key: it is not a parameter, and no tenant header is sent. A refusal throws
292
+ the new `RevTurbineBuiltinDimensionsError` (`status`, `requestId`, machine
293
+ `code`, typed `reason`: `invalid_request` / `unauthorized` / `forbidden` /
294
+ `request_failed`). It never carries the key.
295
+ - **The overlay (all three ports).** `createStaticProviders` takes
296
+ `serverBuiltinDimensions` (from `@revt-eng/core`, re-exported by server-node
297
+ together with `applyServerBuiltinDimensions`). Python's
298
+ `create_static_providers(server_builtin_dimensions=…)` and the
299
+ `RevTurbineCustomerSdk` user context key `server_builtin_dimensions` take
300
+ the same object. So do Rust's `StaticProviderOptions::server_builtin_dimensions`
301
+ and `sdk::UserContext::server_builtin_dimensions`, plus the
302
+ `apply_server_builtin_dimensions` helper in both ports. The object is
303
+ overlaid per key on the app-set `builtin_dimensions` before segment
304
+ membership is evaluated. A delivered leaf wins over the app's value (PD-3),
305
+ and an app-set leaf the server did not deliver is kept. This is the overlay
306
+ the browser SDK already applies to a client-context delivery. Without a user
307
+ context it is ignored, because nothing is evaluated then.
308
+ - **Python and Rust get no HTTP client.** Neither port has a transport or a
309
+ server-key client (BL-0354), and this change does not add one. The app's
310
+ backend reads the endpoint itself (or through server-node) and passes the
311
+ response's `builtin_dimensions` in.
312
+
313
+ Rust callers that build `StaticProviderOptions` or `sdk::UserContext` as a
314
+ full struct literal without `..Default::default()` must add the new field.
315
+
316
+ The `@revt-eng/*` pins move to `0.1.373`, which adds the operation and the
317
+ core overlay (scaffold #430).
318
+
319
+ **Landed in:** 0.11.14.
320
+ **Fail-closed in:** n/a (additive).
321
+ **Proving test:** `tests/server-node-get-builtin-dimensions.test.ts`,
322
+ `server-python/tests/adapters/test_static.py`,
323
+ `server-rust/tests/static_providers.rs`, and the parity fixtures
324
+ `segment_membership_server_overlay_wins` / `segment_membership_server_overlay_demotes`.
325
+
326
+ ### `RevTurbineProvider` disposes the previous SDK instance on an `options` change (BL-0375)
327
+
328
+ **What changed.** `RevTurbineProvider`'s init effect creates a new SDK instance
329
+ whenever the `options` prop (or `bootstrapPlacements`) changes identity, but its
330
+ cleanup previously only flipped an internal `mounted` flag — it never called
331
+ `dispose()` on the instance it was replacing. A host that rebuilds `options` on
332
+ every render (an unmemoized `options={{ ... }}` literal, or an object built in
333
+ an un-annotated `useMemo` with an unstable dependency — the shape web's dogfood
334
+ provider had until web #944) therefore re-initialized on every render and
335
+ leaked every earlier instance's flush-interval timer, `pagehide` /
336
+ `visibilitychange` listeners, and any buffered telemetry.
337
+
338
+ The cleanup now calls the previous instance's existing `dispose()` (flushes
339
+ pending telemetry, detaches its listeners and timer) before the next render's
340
+ instance is constructed. The re-init contract is unchanged: a new `options`
341
+ object identity — even one with identical content — still creates a new SDK
342
+ instance; that documented behavior was never the bug, the leak was. When a new
343
+ `options` object is recreated with the same content as the previous one, the
344
+ provider now also logs a one-time (per mounted provider lifetime) development
345
+ warning pointing integrators at memoizing `options`, in addition to the
346
+ existing every-time identity-change warning.
347
+
348
+ **Landed in:** no release — no signature changed; `dispose()` on
349
+ `RevTurbineCustomerSdk` already existed (documented for manual SPA-unmount
350
+ teardown) and is now also called automatically by the provider.
351
+ **Fail-closed in:** n/a — purely a leak fix; no customer-visible API changed.
352
+ **Proving test:**
353
+ `web-sdk/react/RevTurbineProvider.dispose-on-options-change.test.tsx`.
354
+
355
+ ### Behaviour fix: server SDKs stop serving segment-chipped payloads to every user (BL-0369)
356
+
357
+ **What changed.** On the server lanes, every configured Playbook segment
358
+ counted as a segment the user was in. That covered the Python and Rust
359
+ `RevTurbineCustomerSdk`, `LocalRuntime`, and server-node's
360
+ `LocalEvaluationServer` fed by `createStaticProviders`. So a payload chipped
361
+ to any segment was shown to every user, and an entitlement rule targeted at
362
+ any segment granted to every user on the plan. This included the generated
363
+ built-in segments (`rt.<dimension>.<value>`, e.g.
364
+ `rt.subscription_state.paid`) that hosted Playbooks now export. The browser
365
+ SDK was not affected.
366
+
367
+ A user is now in a segment only when one of these is true, the same rule the
368
+ browser SDK applies:
369
+
370
+ - your app passed its handle in `segment_ids` (Python / Rust) or
371
+ `segmentIds` (`createStaticProviders`), or
372
+ - its predicates match the user's targeting traits. Those traits are built
373
+ from `custom`, the plan, and the reserved `rt_<dimension>` traits.
374
+ `rt_<dimension>` traits come **only** from `builtin_dimensions`, which the
375
+ server sets (plan 279 PD-3). For experiment segments, the user's
376
+ `experiments` assignments decide enrollment.
377
+
378
+ Anything else fails closed. An unset dimension, a trait the user doesn't
379
+ carry, or a chip that names an unknown segment serves nothing (PD-4).
380
+
381
+ New inputs (all additive): `custom`, `builtin_dimensions` and `experiments`
382
+ on the Python `UserContext` and the Rust `UserContext` struct. `segment_ids`
383
+ now also drives chips and rules, not just the catalog helpers.
384
+ `createStaticProviders` gets `segmentIds` and `userContext` from
385
+ `@revt-eng/core`. A side fix: the Python and Rust rules providers now carry
386
+ `segment_dimensions` and the Playbook's `format_version`, like TypeScript.
387
+ Without `segment_dimensions`, a rule targeted at segments in two dimensions
388
+ would have matched on either one once membership became real.
389
+
390
+ **Impact on existing integrations.** This is a correctness fix, but you may
391
+ notice it. If a server-side integration shows segment-chipped payloads or
392
+ grants segment-targeted entitlements, and it passes none of the inputs
393
+ above, those payloads now **stop showing** and those rules **stop
394
+ granting**. They were being over-served before. To keep serving users who
395
+ really are in the segment, pass `segment_ids` for segments your app
396
+ resolves, and `builtin_dimensions` / `custom` for predicate and built-in
397
+ segments. The Rust `UserContext` struct gains three fields. A struct literal
398
+ that does not end in `..Default::default()` needs them added.
399
+
400
+ The `@revt-eng/*` pins move to `0.1.372`: scaffold #429 is the BL-0369 core
401
+ fix, and it carries scaffold #428 (0.1.371: the `billing_health`
402
+ built-in dimension leaf, BL-0317, and `seat_type_handle` becoming
403
+ required-nullable on `ServerUserContextUpsert`, BL-0367). The vendored port
404
+ types follow: in Python, `ServerUserContextUpsert.seat_type_handle` loses its
405
+ default; in Rust, `ClientContextBuiltinDimensionsBillingHealth` is added.
406
+
407
+ **Landed in:** 0.11.14 (`@revt-eng/core` 0.1.372 carries
408
+ the scaffold BL-0369 fix).
409
+ **Fail-closed in:** the same release.
410
+ **Proving test:**
411
+ - `tests/parity/fixtures/segment_membership_{no_inputs,builtin_and_traits,explicit_ids}.json`
412
+ (TS == Python == Rust).
413
+ - `server-python/tests/adapters/test_static.py` and
414
+ `server-rust/tests/static_providers.rs` (segment membership tests).
415
+ - `tests/server-node-local-segment-membership.test.ts`.
416
+
417
+ ### `update()` carries `experiments` and `builtin_dimensions`; server-computed keys are dropped at every browser entry point (BL-0352, ruling D-36)
418
+
419
+ **What changed.** `update()`'s exhaustiveness check never ran. It was an
420
+ `expectTypeOf` inside a vitest `.test.ts`, and vitest does not typecheck. Four
421
+ fields had reached `RevTurbineUpdateInput` without reaching
422
+ `RECOGNIZED_UPDATE_KEYS`, so `update()` accepted them at compile time and
423
+ silently dropped them at runtime. Each one now has an explicit disposition:
424
+
425
+ - *(`builtin_dimensions` superseded by BL-0381 above: it is server-assigned
426
+ and dropped.)* `experiments` and `builtin_dimensions` are **now applied**
427
+ by `update()`.
428
+ The app may supply experiment assignments it already knows
429
+ (`runtime/experiment-assignment.md` §3, plan 183), and it may seed built-in
430
+ dimensions, for example in `local_only` mode. The client-context overlay
431
+ still wins when it is present (`targeting-studio-ui.md` §4.1). Before this
432
+ change, `update({ experiments })` compiled and did nothing.
433
+ - `activity_score` and `activity_score_computed_at` are **server-computed**.
434
+ The control plane's hourly activity-score refresh writes them (plan 180),
435
+ and the browser receives only the derived `activity_level` dimension. Like
436
+ `seat_type_handle` (BL-0351), they are now omitted from
437
+ `RevTurbineUserContext`, `RevTurbineUpdateInput` and `IdentifyContextInput`.
438
+ - `setUserContext()` and the `user` init option now **drop server-assigned
439
+ keys at runtime** (`seat_type_handle`, `activity_score`,
440
+ `activity_score_computed_at`) and report them. `identify()` and `update()`
441
+ already dropped them, and now report them with a server-assigned warning
442
+ that names the backend as the writer, not the "pass it under `custom`"
443
+ hint. Before this change, a plain-JS `setUserContext({ seat_type_handle })`
444
+ merged the key into the held context, and local mode persisted it. A state
445
+ blob persisted before this change is purged of those keys when it is
446
+ restored. A server-action result that echoes them back is stripped without
447
+ a warning.
448
+ - New exported types `RecognizedUpdateKey` and `RecognizedIdentifyKey`
449
+ (additive) back a real compile-time derivation in
450
+ `web-sdk/update-keys-exhaustiveness.test-d.ts`, which
451
+ `pnpm check:types:exact` compiles. A future `UserContext` field fails the
452
+ build until it is added to the runtime list or omitted as server-assigned.
453
+
454
+ **Deferred to `0.12.0`:** `setUserContext()` still takes
455
+ `RevTurbineUserContext` without `Exact<>`, so other undeclared keys that
456
+ arrive through a variable are not rejected at compile time. Adding `Exact<>`
457
+ would break callers that pass a wider object today. `IdentifyContextInput`
458
+ also still declares 11 keys that `identify()` drops with a warning (`trial`,
459
+ `tiers`, `experiments`, …). The type-test pins that list, so it cannot grow
460
+ silently. Narrowing the type is a breaking change and waits for the minor.
461
+
462
+ The `@revt-eng/*` pins move to `0.1.370` (scaffold #427: discovery-owned
463
+ `SurfaceSlotSchema.slot_name`). That field now appears in the generated client
464
+ types and the vendored Python and Rust port types. The SDK does not write it.
465
+
466
+ **Landed in:** 0.11.14.
467
+ **Fail-closed in:** the same release. The compile-time omission and the
468
+ runtime drop at all four entry points both ship in it.
469
+ **Proving test:**
470
+ - `web-sdk/update-keys-exhaustiveness.test-d.ts` (`pnpm check:types:exact`).
471
+ - `web-sdk/identify-guardrails.test.ts`: describe blocks
472
+ `server-computed keys never enter the browser context`,
473
+ `setUserContext() drops server-assigned keys at its runtime boundary` and
474
+ `update() carries app-owned experiments` (renamed by BL-0381).
475
+
476
+ ### server-node `assignSeatType()` writes a user's seat type (BL-0354, plan 279 TASK-16a, ruling D-36)
477
+
478
+ **What changed.** Additive. `RevTurbineServer` (server-node) gains
479
+ `assignSeatType(userId: string, seatTypeHandle: string | null)`. It calls
480
+ `upsertServerUserContext` (`POST /api/sdk/user-contexts`) through the generated
481
+ typed client with the server key the client already holds. The request body is
482
+ exactly `{ user_id, seat_type_handle }`. `null` clears the assignment. The
483
+ tenant comes from the key: it is not a parameter, and no tenant header is sent.
484
+ It resolves to the stored assignment
485
+ `{ tenant_id, user_id, updated_at, seat_type_handle }` (re-exported as
486
+ `ServerUserContextAssignment`).
487
+
488
+ A refusal throws the new `RevTurbineSeatAssignmentError`. It carries `status`,
489
+ `requestId`, the control plane's machine `code` when present, and a typed
490
+ `reason`: `unknown_seat_type` (422: the handle is not one of the tenant's seat
491
+ types), `forbidden` (403: the credential is not a server key), `unauthorized`
492
+ (401), `invalid_request` (400) or `request_failed`. Like
493
+ `RevTurbineClientSessionError`, it never carries the key or the free-text
494
+ response message.
495
+
496
+ Per D-36 the seat assignment is server-written only, so there is no browser SDK
497
+ surface for this. The Python and Rust ports have no HTTP client or server-key
498
+ client, only the vendored `ServerUserContextUpsert` / `ServerUserContextAssignment`
499
+ models, so they gain no helper.
500
+
501
+ The `@revt-eng/*` pins move to `0.1.369`, which adds that operation to the
502
+ external contract (scaffold #425).
503
+
504
+ **Landed in:** 0.11.14.
505
+ **Fail-closed in:** n/a (additive).
506
+ **Proving test:** `tests/server-node-assign-seat-type.test.ts`.
507
+
508
+ ### `seat_type_handle` is server-assigned and never a browser input (BL-0351, plan 279 TASK-16a, ruling D-36)
509
+
510
+ **What changed.** Scaffold `0.1.368` adds `UserContextSchema.seat_type_handle`,
511
+ the user's persisted seat-type assignment. Per D-36 it has exactly two writers:
512
+ the tenant's backend (a server-key user-context upsert) and RevTurbine's own
513
+ server-side enrichment in hosted mode. The browser never writes it. Without a
514
+ change here the new field would have flowed into the browser input types
515
+ through `UserContextInput`. Instead:
516
+
517
+ - `RevTurbineUserContext` omits `seat_type_handle`. So do
518
+ `RevTurbineUpdateInput`, which derives from it, and `IdentifyContextInput`.
519
+ `identify()`, `update()` and the `user` init option reject the key at compile
520
+ time, including when it arrives through a variable (`Exact<…>`).
521
+ `setUserContext()` rejects it in an object literal.
522
+ - The key stays out of the runtime recognized-key lists, so a plain-JS caller's
523
+ `seat_type_handle` is dropped at the `identify()`/`update()` boundary and
524
+ reported by the existing unrecognized-key warning. It is never merged into the
525
+ held context.
526
+ - The browser receives the **resolved** seat type as
527
+ `builtin_dimensions.seat_type` / `builtin_dimensions.buyer_role` from the
528
+ client-context fetch (plan 279 TASK-16, web), unchanged here.
529
+
530
+ No released SDK type ever carried `seat_type_handle`, so nothing an
531
+ integration compiles today stops compiling. The `@revt-eng/*` pins move to
532
+ `0.1.368`. That version also adds `SeatTypeSchema.is_buyer` (default `false`),
533
+ which now appears in the vendored Python and Rust port types.
534
+
535
+ A server-node `assignSeatType(userId, handle | null)` helper was **not** in this
536
+ change, because the server-key operation it wraps (`upsertServerUserContext`,
537
+ `POST /api/sdk/user-contexts`, plan 279 TASK-15a) was not yet in the published
538
+ external contract. It shipped with scaffold `0.1.369` (see the BL-0354 entry
539
+ above).
540
+
541
+ **Landed in:** 0.11.14. This PR bumps pins only.
542
+ **Fail-closed in:** the same release (compile-time rejection; runtime drop).
543
+ **Proving test:**
544
+ - `web-sdk/user-context-exactness.test-d.ts`: the `@ts-expect-error` block
545
+ "Server-assigned fields" (`pnpm check:types:exact`).
546
+ - `web-sdk/identify-guardrails.test.ts`: describe block
547
+ `seat_type_handle never enters the browser context`.
548
+
549
+ ## 0.11.13
550
+
551
+ ### Slot lifecycle events carry the slot's `route` (BL-0207, ruling D-30)
552
+
553
+ **What changed.** Every slot lifecycle event (`slot_evaluated`, `slot_filled`,
554
+ `slot_empty`, `slot_suppressed`, `slot_error`) now carries `route`: the app
555
+ route the slot rendered on. Ingestion-driven surface-slot discovery reads it
556
+ and persists it on the discovered slot. There is no SDK write to any CRUD
557
+ route. The field is additive and optional in the scaffold contract
558
+ (`slotContextBase.route`).
559
+
560
+ A route is a **path, never a URL**. It has no origin, query string or
561
+ fragment. The React layer computes it at emission time:
562
+
563
+ - **Default:** `window.location.pathname`, with identifier-like segments
564
+ templated to `:id`. That covers numbers, UUIDs, long hex, prefixed ids like
565
+ `cus_…`, long opaque tokens and email-shaped values, raw or
566
+ percent-encoded. For example, `/accounts/4821/billing?coupon=X` reports
567
+ `/accounts/:id/billing`.
568
+ - **`<RevTurbineRoute route={…}>`** (new, renderless) declares the framework
569
+ route pattern for its subtree, and the innermost boundary wins.
570
+ - **Next.js App Router:** build the pattern with the new pure helper
571
+ `routePatternFromParams(usePathname(), useParams())`, which gives
572
+ `/projects/[projectId]/settings`. The SDK does not import `next/navigation`.
573
+
574
+ Also new:
575
+
576
+ - **Headless exports:** `normalizeSlotRoute`, `routePatternFromParams`,
577
+ `currentBrowserRoute`, `isIdentifierLikeSegment` and `SLOT_ROUTE_MAX_LENGTH`
578
+ (512).
579
+ - **React hook:** `useSlotRoute()`.
580
+ - **Headless controller option:** `PlacementControllerOptions.route` accepts a
581
+ string or a getter. When it is omitted, a headless controller emits
582
+ `route: null`.
583
+
584
+ **Landed in:** 0.11.13 (`web-sdk` only; `server-python`/`server-rust` bumped
585
+ in lockstep with no behavior change — no port emits slot events). The
586
+ `@revt-eng/*` pins move to `0.1.366` (the scaffold contract: `slotContextBase.route`,
587
+ `SurfaceSlotSchema.route`, and the `SurfaceSlotStatus` = `live | idle`
588
+ vocabulary), and the vendored port types are re-synced — `SurfaceSlot.status`
589
+ is now `SurfaceSlotStatus` (`live`/`idle`, default `idle`) in both ports.
590
+ **Fail-closed in:** n/a (additive telemetry field).
591
+ **Proving test:**
592
+ - `web-sdk/slot-route.test.ts`: normalization, PII/query stripping, the
593
+ Next.js params recipe, and controller emission including the
594
+ throwing-getter case.
595
+ - `web-sdk/react/usePlacement.route.test.tsx`: the plain-DOM pathname case and
596
+ the `<RevTurbineRoute>` Next.js pattern case.
597
+
50
598
  ## 0.11.12
51
599
 
52
600
  ### `builtin_dimensions` now reaches the browser SDK from the client-context fetch (BL-0312, plan 279 TASK-5)
@@ -226,11 +226,11 @@ declare const UserPlanContextSchema: z.ZodObject<{
226
226
  */
227
227
  declare const UserBuiltinDimensionsSchema: z.ZodObject<{
228
228
  activity_level: z.ZodOptional<z.ZodEnum<{
229
- inactive: "inactive";
230
229
  new: "new";
231
230
  high: "high";
232
231
  medium: "medium";
233
232
  low: "low";
233
+ inactive: "inactive";
234
234
  }>>;
235
235
  subscription_state: z.ZodOptional<z.ZodEnum<{
236
236
  trial: "trial";