@revturbine/sdk 0.11.13 → 0.11.17
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 +681 -0
- package/dist/headless.d.ts +3701 -102
- package/dist/headless.js +2 -2
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +3701 -102
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/server-node/client.d.ts +211 -3
- package/dist/types/server-node/client.d.ts.map +1 -1
- package/dist/types/server-node/index.d.ts +15 -3
- package/dist/types/server-node/index.d.ts.map +1 -1
- package/dist/types/server-node/local-server.d.ts +15 -2
- package/dist/types/server-node/local-server.d.ts.map +1 -1
- package/dist/types/server-node/types.d.ts +67 -2
- package/dist/types/server-node/types.d.ts.map +1 -1
- package/dist/types/web-sdk/config-artifact.d.ts +7 -2
- package/dist/types/web-sdk/config-artifact.d.ts.map +1 -1
- package/dist/types/web-sdk/controllers.d.ts.map +1 -1
- package/dist/types/web-sdk/customer-side.d.ts +498 -36
- package/dist/types/web-sdk/customer-side.d.ts.map +1 -1
- package/dist/types/web-sdk/local-builtin-segments.d.ts +46 -0
- package/dist/types/web-sdk/local-builtin-segments.d.ts.map +1 -0
- package/dist/types/web-sdk/react/RevTurbineProvider.d.ts.map +1 -1
- package/dist/types/web-sdk/theme/theme-loader.d.ts +12 -2
- package/dist/types/web-sdk/theme/theme-loader.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -47,6 +47,687 @@ also require a changelog entry.
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 0.11.17
|
|
51
|
+
|
|
52
|
+
### Checkout metadata helper, `checkout_started`, and decision context on interactions (plan 282 TASK-9, BL-0432)
|
|
53
|
+
|
|
54
|
+
**What changed.** Three additive changes to the browser SDK (`@revturbine/sdk`);
|
|
55
|
+
no existing signature narrows.
|
|
56
|
+
|
|
57
|
+
- **`checkoutMetadata(outputId)`** (new, `@public`) returns the Stripe Checkout
|
|
58
|
+
`revturbine_*` metadata bag for an output this SDK decided — user, account,
|
|
59
|
+
placement, decision, output, rule and target plan — plus
|
|
60
|
+
`client_reference_id` (the user id). Pass `metadata` as both the session's
|
|
61
|
+
`metadata` and `subscription_data.metadata`; the mirror is what reaches each
|
|
62
|
+
invoice's `parent.subscription_details.metadata`. A key the SDK does not hold
|
|
63
|
+
is absent, never empty; `null` for an output this SDK did not decide. New
|
|
64
|
+
types `RevTurbineCheckoutMetadata` and `RevTurbineCheckoutMetadataBag`.
|
|
65
|
+
- **`checkout_started`** (taxonomy v6, `@revt-eng/schema` 0.1.381) is emitted on
|
|
66
|
+
a `cta_clicked` whose CTA opens a plan picker or pricing page
|
|
67
|
+
(`CHECKOUT_STARTED_CTA_ACTION_TYPES`: `navigate_to_plans`, `view_plans`) with
|
|
68
|
+
`{ placement_id, payload_id, decision_id, plan_handle, rule_handle }`. Never
|
|
69
|
+
for `open_checkout_modal` — that CTA opens checkout directly ("on click"), so
|
|
70
|
+
its started count is its clicked count.
|
|
71
|
+
- **`trackTreatmentInteraction`** accepts optional `decisionId`,
|
|
72
|
+
`segmentHandles` and `segmentIds` (`RevTurbineTreatmentInteractionDecisionContext`;
|
|
73
|
+
the parameter type is now `RevTurbineTreatmentInteractionRequest`, a
|
|
74
|
+
superset of the previous `RevTurbineTreatmentInteractionInput`) and sends
|
|
75
|
+
them as `decision_id` / `segment_handles` / `segment_ids`. Segments are
|
|
76
|
+
handles: `segment_handles` is the effective set scaffold's `evaluateSegments`
|
|
77
|
+
built plus any hosted-resolved slugs — the analytics join key — and
|
|
78
|
+
`segment_ids` carries only the minted ids a hosted context supplied
|
|
79
|
+
(telemetry; omitted entirely in local mode). When not given, they are filled
|
|
80
|
+
from the output this SDK indexed for `payloadId`; a caller that gives none
|
|
81
|
+
and names no indexed output sends exactly the row it sent before. The React
|
|
82
|
+
controller's clicks and the output-addressed `convert()` / `dismiss()` /
|
|
83
|
+
`snooze()` carry them.
|
|
84
|
+
|
|
85
|
+
**Landed in** 0.11.17. **Fail-closed in** n/a — additive. The headless ports
|
|
86
|
+
(Python, Rust, `server-node`) carry only the re-vendored generated types for
|
|
87
|
+
the 0.1.381 pin; the cross-language parity corpus is untouched.
|
|
88
|
+
|
|
89
|
+
**Docs.** `guides/billing.mdx` "Where checkout fits" now documents the bag, the
|
|
90
|
+
`subscription_data.metadata` mirror, `client_reference_id`, and that Customer
|
|
91
|
+
metadata cannot be set from Checkout.
|
|
92
|
+
|
|
93
|
+
**Proving tests.** `web-sdk/checkout-metadata.test.ts`,
|
|
94
|
+
`web-sdk/checkout-started-emit.test.ts`,
|
|
95
|
+
`web-sdk/interaction-decision-context.test.ts`.
|
|
96
|
+
|
|
97
|
+
### `update({ usage })` now reaches usage and credit threshold placements and their usage tokens (BL-0402, BL-0409)
|
|
98
|
+
|
|
99
|
+
**What changed.** A behaviour fix in the browser SDK (`@revturbine/sdk`); no
|
|
100
|
+
signature changes. `update({ usage })` / `updateUsage()` wrote only the SDK's
|
|
101
|
+
private usage balances. `can()` and `getUsage()` read those balances, but the
|
|
102
|
+
placement resolver built its usage state from the user context's `usage`
|
|
103
|
+
entries alone, and took each entry's `limit` only from a per-entry field
|
|
104
|
+
(default `0`). The shared threshold gate fails closed on a missing or zero
|
|
105
|
+
limit. So `usage_threshold` and `credit_threshold` placements (the 80% usage
|
|
106
|
+
banner, the low-credits warning) never fired for app-reported usage, and
|
|
107
|
+
`{{usage_current}}` / `{{usage_limit}}` / `{{usage_remaining}}` /
|
|
108
|
+
`{{usage_percent}}` stayed stale or unresolved, while the gate and the meter
|
|
109
|
+
moved.
|
|
110
|
+
|
|
111
|
+
Placements now read one usage view, the same one `getUsage()` reports:
|
|
112
|
+
|
|
113
|
+
- **Used**: the balance the app reported through `update({ usage })` /
|
|
114
|
+
`updateUsage()` / `identify(…, { usage })` wins (the most recent write),
|
|
115
|
+
else the context entry's `amount`.
|
|
116
|
+
- **Limit**: the context entry's own `limit` wins, else the Playbook
|
|
117
|
+
allowance for the user's **current plan** (the plan-scoped `usage_limit`
|
|
118
|
+
rule's `limit_value` or `credits` rule's `allowance_value`, which is the
|
|
119
|
+
`limit` `getUsage()` already returned). An `unlimited` or absent allowance
|
|
120
|
+
means no threshold: the placement does not fire.
|
|
121
|
+
- **Tokens**: the SDK now fills the usage token family in decision content
|
|
122
|
+
from the same view. It fills `{{usage_current}}` whenever usage is known.
|
|
123
|
+
It fills `{{usage_limit}}`, `{{usage_remaining}}` and `{{usage_percent}}`
|
|
124
|
+
only when a positive limit is known, so an unlimited allowance leaves
|
|
125
|
+
those tokens untouched instead of rendering `0`. The `<unit>_usage_*`
|
|
126
|
+
forms and the aliases (`current_usage`, `current_limit`,
|
|
127
|
+
`remaining_usage`) are filled too. A registered entitlements provider
|
|
128
|
+
still supplies its own usage state, as before.
|
|
129
|
+
|
|
130
|
+
`can()` and `getUsage()` are unchanged. The headless server ports (Python,
|
|
131
|
+
Rust, `server-node`) take usage counters from the caller and are unaffected;
|
|
132
|
+
the cross-language parity corpus is untouched.
|
|
133
|
+
|
|
134
|
+
**Docs.** `tutorials/usage-quota-meter.mdx` and
|
|
135
|
+
`tutorials/low-credits-warning.mdx` now use the canonical flat Playbook rule
|
|
136
|
+
shape (`kind` + `limit_value` / `allowance_value`, handle-valued ids). The
|
|
137
|
+
old nested `type_fields` shape resolved no allowance, so neither tutorial's
|
|
138
|
+
banner could fire. The expected-behaviour table now matches what the SDK
|
|
139
|
+
does: the banner shows from 80%, and at 100% `can('api_calls')` denies.
|
|
140
|
+
|
|
141
|
+
**Proving test.** `web-sdk/usage-reaches-placements.test.ts`.
|
|
142
|
+
|
|
143
|
+
### App-supplied trial context now reaches trial placements, trial tokens and reverse-trial grants (BL-0403)
|
|
144
|
+
|
|
145
|
+
**What changed.** A behaviour fix in the browser SDK (`@revturbine/sdk`); no
|
|
146
|
+
signature changes. `user.trial` at init, `update({ trial })`,
|
|
147
|
+
`setUserContext({ trial })` and a server action's `userContext` result all
|
|
148
|
+
merged into the user context's `trial`, but neither trial reader looked there.
|
|
149
|
+
The placement resolver's plan state (which gates `trial_started`,
|
|
150
|
+
`trial_progress`, `trial_ending`, `trial_ended` and `trial_converted`
|
|
151
|
+
placements) and the reverse-trial entitlement grants read only the SDK's own
|
|
152
|
+
trial status, written by `localRuntime.initialData.trialStatus`, `hydrate()`,
|
|
153
|
+
`getTrialStatus()` and `setTrialInstances()`. So an app that supplied its
|
|
154
|
+
trial saw no trial placement fire, no reverse-trial grant apply, and
|
|
155
|
+
`{{trial_days_remaining}}` stay raw.
|
|
156
|
+
|
|
157
|
+
Both readers now use one trial view:
|
|
158
|
+
|
|
159
|
+
- **Sources.** The user context's `trial` (what the app supplies, plus the
|
|
160
|
+
authenticated client-context delivery, which merges its trial into the same
|
|
161
|
+
field) and the SDK trial status (`initialData.trialStatus`, `hydrate()`,
|
|
162
|
+
`getTrialStatus()`, `setTrialInstances()`).
|
|
163
|
+
- **Precedence: the most recent change wins**, the same rule the usage view
|
|
164
|
+
applies to reported balances (BL-0402). A server-delivered or hydrated
|
|
165
|
+
status wins when it arrives after the app's trial (an
|
|
166
|
+
`initialData.trialStatus` passed alongside init `user.trial` counts as
|
|
167
|
+
later). An app `update({ trial })` made after it wins over it. Re-sending
|
|
168
|
+
an unchanged trial is not a newer write, so passing the same trial on every
|
|
169
|
+
render never displaces a fresher server status. A source that was never
|
|
170
|
+
supplied does not compete.
|
|
171
|
+
- **Tokens.** The SDK now fills `{{trial_days_remaining}}` and
|
|
172
|
+
`{{trial_days_total}}` in decision content from the same plan state the
|
|
173
|
+
trial gate read. A token whose value is unknown is left untouched.
|
|
174
|
+
- **`getTrialStatus()`** in `local_only` mode with no `getTrialStatus`
|
|
175
|
+
resolver returns this trial view, the app-supplied trial included.
|
|
176
|
+
|
|
177
|
+
**React.** `<RevTurbineProvider>` no longer reports `trial`, `tiers`,
|
|
178
|
+
`payment_failed`, `payment_at_risk`, `instances`, `experiments`, `email_type`,
|
|
179
|
+
`derived_config_version`, `context_hash` or `derived_computed_at` in
|
|
180
|
+
`options.user` as "dropped unrecognized user-context key(s)" on mount. The SDK
|
|
181
|
+
constructor already merges them from `options.user`; the provider now forwards
|
|
182
|
+
only the identity fields `identify()` merges. Unknown keys and server-assigned
|
|
183
|
+
keys are still reported.
|
|
184
|
+
|
|
185
|
+
The headless server ports (Python, Rust, `server-node`) take an
|
|
186
|
+
already-derived trial status and are unaffected; the cross-language parity
|
|
187
|
+
corpus is untouched.
|
|
188
|
+
|
|
189
|
+
**Proving tests.** `web-sdk/trial-reaches-placements.test.ts`,
|
|
190
|
+
`web-sdk/react/RevTurbineProvider.identify-context.test.tsx`.
|
|
191
|
+
|
|
192
|
+
### Trial state no longer leaks across users; app-supplied trials fire lifecycle triggers; `getUserContext()` reports trial and billing fields (BL-0417)
|
|
193
|
+
|
|
194
|
+
**What changed.** Behaviour fixes in the browser SDK (`@revturbine/sdk`)
|
|
195
|
+
that follow up BL-0403. No signature changes.
|
|
196
|
+
|
|
197
|
+
- **Reset and user switch.** `resetIdentity()` / `resetUserContext()`
|
|
198
|
+
cleared the user context but kept the SDK trial status (from
|
|
199
|
+
`initialData.trialStatus`, `hydrate()`, `getTrialStatus()` or
|
|
200
|
+
`setTrialInstances()`). They also kept which trial source was newest and
|
|
201
|
+
the last lifecycle stage. So the next user saw the previous user's trial
|
|
202
|
+
placements, reverse-trial grants and `{{trial_days_*}}` tokens, and a
|
|
203
|
+
trial-less next user could emit `trial_expired`. A reset now clears all of
|
|
204
|
+
that, and the next user starts with no trial. Identifying a **different**
|
|
205
|
+
user without a reset (`identify()`, `setUserContext({ id })`, or a
|
|
206
|
+
`hydrate()` payload for another user) now drops the previous user's trial
|
|
207
|
+
(both sources) and usage (reported balances and context `usage`) before
|
|
208
|
+
merging the new user's values. The new user's own values still apply. Going
|
|
209
|
+
from anonymous to identified, or re-identifying the same user, keeps what
|
|
210
|
+
is held. Reported usage was already cleared on reset; only the
|
|
211
|
+
switch-without-reset path leaked it.
|
|
212
|
+
- **Lifecycle triggers.** `trial_midpoint`, `trial_expiring` and
|
|
213
|
+
`trial_expired` fired only from `getTrialStatus()` and
|
|
214
|
+
`setTrialInstances()`. The SDK now also runs the lifecycle evaluation
|
|
215
|
+
against the resolved trial view after every change that can move it:
|
|
216
|
+
`user.trial` at init, `update({ trial })`, `setUserContext`, a server
|
|
217
|
+
action's context, the client-context delivery, `hydrate()`, a reset, and a
|
|
218
|
+
Playbook load that turns the triggers on. The dedupe has not changed: a
|
|
219
|
+
stage fires only when it differs from the last stage seen. So an unchanged
|
|
220
|
+
trial, or both sources reporting the same threshold, fires once.
|
|
221
|
+
`enableTrialAutoTriggers: false` still silences them.
|
|
222
|
+
- **`getUserContext()`** now includes `trial` (the resolved trial view, the
|
|
223
|
+
one placements and grants decide with), `tiers`, `payment_failed`,
|
|
224
|
+
`payment_at_risk` and `instances` when they are held. The snapshot is a
|
|
225
|
+
copy, so changing it does not change SDK state.
|
|
226
|
+
|
|
227
|
+
The headless server ports are unaffected, and the cross-language parity
|
|
228
|
+
corpus is untouched.
|
|
229
|
+
|
|
230
|
+
**Proving test.** `web-sdk/trial-reset-lifecycle.test.ts`.
|
|
231
|
+
|
|
232
|
+
## 0.11.16
|
|
233
|
+
|
|
234
|
+
### Build fix, part two: the npm declaration build resolves `openapi-fetch` too (no API change)
|
|
235
|
+
|
|
236
|
+
**What changed.** 0.11.15 fixed the bundle step, but the public npm publish
|
|
237
|
+
then failed one command later, in the declaration emit
|
|
238
|
+
(`tsc -p tsconfig.build.json`). It reported
|
|
239
|
+
`../server-node/client.ts(29,43): error TS2307: Cannot find module 'openapi-fetch'`,
|
|
240
|
+
for the same reason as before: TypeScript resolves the bare import by walking
|
|
241
|
+
up from `server-node/`, and the public repo installs the package only under
|
|
242
|
+
`web-sdk/`. `web-sdk/tsconfig.build.json` now maps `openapi-fetch` to
|
|
243
|
+
`./node_modules/openapi-fetch`.
|
|
244
|
+
|
|
245
|
+
**npm users:** the 0.11.14 changes first reach npm in 0.11.16. Neither 0.11.14
|
|
246
|
+
nor 0.11.15 was published to npm (both are on GitHub Packages, PyPI and
|
|
247
|
+
crates.io). There is no behavior change across 0.11.14 through 0.11.16.
|
|
248
|
+
|
|
249
|
+
**Landed in:** 0.11.16.
|
|
250
|
+
**Fail-closed in:** n/a (build only).
|
|
251
|
+
**Proving test:** the public repo's `release-npm.yml` build step for
|
|
252
|
+
`v0.11.16`. It was reproduced locally by hiding the root `openapi-fetch` link
|
|
253
|
+
to match the public layout: the declaration emit failed with the same TS2307
|
|
254
|
+
before this change, and tsup, the declaration emit and rollup dts all pass
|
|
255
|
+
after it.
|
|
256
|
+
|
|
257
|
+
## 0.11.15
|
|
258
|
+
|
|
259
|
+
### Build fix: the npm package publishes again (no API change)
|
|
260
|
+
|
|
261
|
+
**What changed.** 0.11.14 was published to GitHub Packages, PyPI and
|
|
262
|
+
crates.io, but the public npm publish of `@revturbine/sdk` failed at the bundle
|
|
263
|
+
step: `Could not resolve "openapi-fetch"` from `server-node/client.ts`, which
|
|
264
|
+
the `headless` entry re-exports. `openapi-fetch` has been bundled since BL-0354
|
|
265
|
+
(0.11.14 was the first release to carry it). A bare import resolves by walking
|
|
266
|
+
up from the importing file, so from `server-node/` it never reached
|
|
267
|
+
`web-sdk/node_modules`, which is the only place the public repo installs it.
|
|
268
|
+
`web-sdk/tsup.config.ts` now gives esbuild `web-sdk/node_modules` as a fallback
|
|
269
|
+
`nodePaths` entry, so every file the SDK bundles resolves the SDK's own
|
|
270
|
+
dependencies.
|
|
271
|
+
|
|
272
|
+
**npm users:** everything listed under 0.11.14 below first reaches npm in
|
|
273
|
+
0.11.15. There is no behavior change between 0.11.14 and 0.11.15. PyPI and
|
|
274
|
+
crates.io get 0.11.15 in lockstep, with no change.
|
|
275
|
+
|
|
276
|
+
**Landed in:** 0.11.15.
|
|
277
|
+
**Fail-closed in:** n/a (build only).
|
|
278
|
+
**Proving test:** the public repo's `release-npm.yml` bundle step (tsup +
|
|
279
|
+
declarations) for `v0.11.15`. Locally, `web-sdk/tsup.config.ts` builds the
|
|
280
|
+
bundle, and an esbuild probe confirms `nodePaths` resolves a bare import from a
|
|
281
|
+
sibling directory that has no `node_modules` of its own.
|
|
282
|
+
|
|
283
|
+
## 0.11.14
|
|
284
|
+
|
|
285
|
+
This release carries the BL-0335 change below plus every entry that main held
|
|
286
|
+
under "Unreleased" since 0.11.13 (BL-0381, BL-0382, BL-0366, BL-0375, BL-0369,
|
|
287
|
+
BL-0352, BL-0354 and BL-0351).
|
|
288
|
+
|
|
289
|
+
### `tenantId` is optional on a browser init with a `publicKey`; the key is the sole arbiter of the tenant (BL-0335, D-49)
|
|
290
|
+
|
|
291
|
+
**What changed.** Before, a browser init needed both a `tenantId` and a
|
|
292
|
+
`publicKey`. The public key is bound to exactly one tenant, and the control
|
|
293
|
+
plane already resolved the tenant from the key alone, so the id was redundant
|
|
294
|
+
configuration, and a wrong one was silently ignored. Now:
|
|
295
|
+
|
|
296
|
+
- `RevTurbineInitOptions.tenantId` is **optional**. The minimal hosted init is
|
|
297
|
+
`{ publicKey, endpoint }`.
|
|
298
|
+
- A key-only integration sends **no** `x-tenant-id`. It reads the tenant from
|
|
299
|
+
the key-authenticated Playbook delivery: the signed manifest's `tenant_id`
|
|
300
|
+
and the Playbook's own `tenant_id`. Until the first Playbook loads, the
|
|
301
|
+
tenant is unknown, so `getUserContext().tenant_id` and the event envelope's
|
|
302
|
+
`tenant_id` read `''` and the `/api/track` row omits `tenant_id`. The
|
|
303
|
+
control plane stamps the key's tenant regardless.
|
|
304
|
+
- A **configured** `tenantId` that matches the key's tenant behaves exactly as
|
|
305
|
+
before: it is still sent, and every locally persisted key (anonymous id,
|
|
306
|
+
decision cache, caps, interaction state) is unchanged.
|
|
307
|
+
- A **configured** `tenantId` that does **not** match the key's tenant is no
|
|
308
|
+
longer silently ignored, and it is not refused either (D-49). When the
|
|
309
|
+
key-authenticated delivery names another tenant, the SDK logs one warning
|
|
310
|
+
per init and adopts the key's tenant:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
[RevTurbine] tenantId <configured> does not match the key's tenant <key tenant>; using <key tenant>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The signed manifest is activated (before, a manifest whose tenant differed
|
|
317
|
+
from the configured one was refused and the SDK fell back to the legacy
|
|
318
|
+
config fetch). After adoption, `getUserContext().tenant_id`, event envelopes
|
|
319
|
+
and the `x-tenant-id` the SDK still sends all carry the key's tenant. Local
|
|
320
|
+
storage keys stay namespaced by the configured id, so nothing persisted is
|
|
321
|
+
lost.
|
|
322
|
+
- A key-only integration keys local state by a short hash of the public key
|
|
323
|
+
(`revturbine:pk-…:`), so rotating the key starts a fresh local namespace,
|
|
324
|
+
just as changing `tenantId` always did.
|
|
325
|
+
- The theme loader (`fetchThemeOverride`) no longer sends the `'local'`
|
|
326
|
+
placeholder as a tenant header. `ThemeLoaderOptions.tenantId` is optional,
|
|
327
|
+
and the new `storageScope` option keeps the persisted-theme key.
|
|
328
|
+
- `ConfigTargetDefaults.tenantId` is optional. Without it, the artifact's own
|
|
329
|
+
`tenant_id` is used.
|
|
330
|
+
- Init now fails loudly when no tenant can be identified. It throws when
|
|
331
|
+
neither `publicKey` nor `tenantId` is present, and when a `local_only` init
|
|
332
|
+
has no `tenantId` and no `localRuntime.playbook`. With a local Playbook,
|
|
333
|
+
`local_only` still defaults the tenant to `'local'`, and a local Playbook's
|
|
334
|
+
`tenant_id` stays a guard behind the init option (plan 233 REQ-2): there is
|
|
335
|
+
no key there to arbitrate.
|
|
336
|
+
|
|
337
|
+
**Server side (web #927).** The control plane uses the key's tenant
|
|
338
|
+
unconditionally on every public-key lane. A tenant id that IS sent, whether as
|
|
339
|
+
a header or as `tenant_id` in an event or interaction body, is diagnostic only:
|
|
340
|
+
a mismatch is logged server-side and ignored. No request is refused for it.
|
|
341
|
+
|
|
342
|
+
**Landed in:** 0.11.14 (`web-sdk` only; `server-python`/`server-rust` bumped
|
|
343
|
+
in lockstep with no behavior change: the server ports authenticate with the
|
|
344
|
+
server key and build a per-request context with a `tenant_id`, and they have
|
|
345
|
+
no browser `publicKey` init option to relax).
|
|
346
|
+
**Fail-closed in:** 0.11.14. An init with no identifiable tenant throws;
|
|
347
|
+
before, it was a type error only, and JS callers got `undefined`. A mismatching
|
|
348
|
+
configured tenant warns from 0.11.14; before, it was silent.
|
|
349
|
+
**Proving test:**
|
|
350
|
+
- `web-sdk/customer-side-tenant-optional.test.ts`: key-only init on both
|
|
351
|
+
delivery lanes (no tenant header; tenant adopted from the manifest and
|
|
352
|
+
Playbook), configured matching tenant unchanged (header sent, storage keys
|
|
353
|
+
byte-identical), a mismatching configured tenant warned once and the key's
|
|
354
|
+
tenant adopted on both lanes, the key-hash namespace, the init guards, and
|
|
355
|
+
the theme loader.
|
|
356
|
+
- `web-sdk/init-options-exactness.test-d.ts`: a key-only init compiles.
|
|
357
|
+
|
|
358
|
+
### server-node `setBuiltinDimensions()` — your backend sets built-in dimension values, with an `override` flag (BL-0382, rulings D-46 / D-47)
|
|
359
|
+
|
|
360
|
+
**What changed.** Additive. The customer's backend can now SET built-in
|
|
361
|
+
segment dimension values for a user, not just read the control plane's
|
|
362
|
+
(D-46: "the customer's backend calling revturbine to set the dimension").
|
|
363
|
+
|
|
364
|
+
- **server-node** — `RevTurbineServer.setBuiltinDimensions(userId, dimensions,
|
|
365
|
+
{ override })` calls the widened `upsertServerUserContext`
|
|
366
|
+
(`POST /api/sdk/user-contexts`) with `{ user_id, builtin_dimensions,
|
|
367
|
+
override }` and the server key. Settable: `subscription_state`,
|
|
368
|
+
`trial_type`, `billing_health`, `activity_level`, `buyer_role`,
|
|
369
|
+
`email_type`, `region`, `device_type` (typed as
|
|
370
|
+
`ServerBuiltinDimensionsWrite`, each a closed vocabulary; `null` clears a
|
|
371
|
+
dimension). `seat_type` stays `assignSeatType`. It resolves to the stored
|
|
372
|
+
state — `seat_type_handle`, the customer-set `builtin_dimensions`, the
|
|
373
|
+
pinned `overrides`, `updated_at`. A refusal throws the new
|
|
374
|
+
`RevTurbineDimensionWriteError` (`status`, `requestId`, machine `code`,
|
|
375
|
+
typed `reason`: `invalid_request` / `unauthorized` / `forbidden` /
|
|
376
|
+
`unknown_dimension_value` / `request_failed`); it never carries the key.
|
|
377
|
+
- **`override` (D-47).** `false` (default) is an UPDATE: the value is delivered
|
|
378
|
+
until a newer RevTurbine enrichment change supersedes it (last writer wins,
|
|
379
|
+
enrichment included), and writing a pinned dimension without the flag
|
|
380
|
+
unpins it. `true` is an OVERRIDE: the dimension is pinned and enrichment
|
|
381
|
+
never replaces it until you write it again without the flag or clear it.
|
|
382
|
+
The control plane applies this before delivery, so the browser SDK's client
|
|
383
|
+
context and `getBuiltinDimensions()` both return the result; the SDK
|
|
384
|
+
delivery rule and the parity goldens are unchanged.
|
|
385
|
+
- **`assignSeatType(userId, handle, { builtinDimensions, override })`** can
|
|
386
|
+
write dimension values in the same upsert. Its `SeatAssignmentErrorReason`
|
|
387
|
+
gains `unknown_dimension_value` (a 422 with code `UNKNOWN_DIMENSION_VALUE`).
|
|
388
|
+
Its response now also carries `builtin_dimensions` and `overrides`.
|
|
389
|
+
- **Python and Rust get no HTTP client** (unchanged: neither port has a
|
|
390
|
+
server-key transport). A backend on those stacks calls the endpoint itself.
|
|
391
|
+
- Browser input types are untouched: `builtin_dimensions` is never a browser
|
|
392
|
+
write path.
|
|
393
|
+
|
|
394
|
+
The `@revt-eng/*` pins move to `0.1.374`, which widens the operation
|
|
395
|
+
(scaffold #431).
|
|
396
|
+
|
|
397
|
+
**Landed in:** 0.11.14.
|
|
398
|
+
**Fail-closed in:** n/a (additive).
|
|
399
|
+
**Proving test:** `tests/server-node-set-builtin-dimensions.test.ts`,
|
|
400
|
+
`tests/server-node-assign-seat-type.test.ts`.
|
|
401
|
+
|
|
402
|
+
### Behaviour change: the browser SDK ignores app-set `builtin_dimensions` (BL-0381, ruling D-46)
|
|
403
|
+
|
|
404
|
+
**What changed.** Trust in a built-in dimension value is decided by *party*
|
|
405
|
+
(Kent, D-46): the browser client never asserts one, the customer's server may,
|
|
406
|
+
and RevTurbine's own enrichment is optional. `builtin_dimensions` therefore
|
|
407
|
+
joins `seat_type_handle`, `activity_score` and `activity_score_computed_at`
|
|
408
|
+
as a server-assigned key in the browser SDK.
|
|
409
|
+
|
|
410
|
+
- **Types.** `builtin_dimensions` is omitted from `RevTurbineUserContext`,
|
|
411
|
+
`RevTurbineUpdateInput` and `IdentifyContextInput`, so
|
|
412
|
+
`update({ builtin_dimensions })`, `setUserContext({ builtin_dimensions })`,
|
|
413
|
+
`identify(id, { builtin_dimensions })` and `user: { builtin_dimensions }`
|
|
414
|
+
no longer compile.
|
|
415
|
+
- **Runtime.** Every browser entry point drops the key and warns with the
|
|
416
|
+
server-assigned diagnostic (`update()`, `identify()`, `setUserContext()`,
|
|
417
|
+
the `user` init option). A persisted `local_only` state blob that carries it
|
|
418
|
+
is purged on restore, and a server-action result that echoes it is stripped
|
|
419
|
+
silently. Before this change, 0.11.12 and 0.11.13 merged an app-set value
|
|
420
|
+
through `setUserContext()` and the `user` option. An app could then claim
|
|
421
|
+
any built-in segment (for example `rt.subscription_state.paid`) from the
|
|
422
|
+
browser.
|
|
423
|
+
- **The one browser source** is the authenticated
|
|
424
|
+
`GET /api/sdk/client-context` delivery (`fetchClientContext`, or a
|
|
425
|
+
`clientSession` minter). Its values overlay per leaf exactly as before.
|
|
426
|
+
`getUserContext()` still returns the held value.
|
|
427
|
+
- **`local_only`.** Built-in segment *definitions* are still synthesized for
|
|
428
|
+
a local Playbook (BL-0365), so chips and rules naming `rt.*` segments stay
|
|
429
|
+
valid. Their *values* no longer come from the app: **a browser `local_only`
|
|
430
|
+
app can no longer target built-ins unless a server-provided context
|
|
431
|
+
delivers them.** With no delivery every `rt_*` trait is absent and the
|
|
432
|
+
built-in segments fail closed (PD-4). `rt.registration_state.*` still
|
|
433
|
+
resolves from the identified id alone.
|
|
434
|
+
- **Unchanged: server SDKs.** They are the customer's server. Their overlay
|
|
435
|
+
inputs stay: server-node's `createStaticProviders({ userContext:
|
|
436
|
+
{ builtin_dimensions }, serverBuiltinDimensions })`, Python's
|
|
437
|
+
`builtin_dimensions` / `server_builtin_dimensions` and Rust's
|
|
438
|
+
`builtin_dimensions` / `server_builtin_dimensions`. Parity goldens are
|
|
439
|
+
unchanged.
|
|
440
|
+
|
|
441
|
+
This supersedes the part of BL-0352 (below) that made `update()` apply
|
|
442
|
+
`builtin_dimensions`. That change never shipped.
|
|
443
|
+
|
|
444
|
+
**Landed in:** 0.11.14.
|
|
445
|
+
**Fail-closed in:** the same release. The compile-time omission and the
|
|
446
|
+
runtime drop ship together.
|
|
447
|
+
**Proving test:**
|
|
448
|
+
- `web-sdk/update-keys-exhaustiveness.test-d.ts` (`pnpm check:types:exact`):
|
|
449
|
+
`builtin_dimensions` is in the server-assigned set.
|
|
450
|
+
- `web-sdk/identify-guardrails.test.ts`: describe block
|
|
451
|
+
`builtin_dimensions never enters the browser context from the app`.
|
|
452
|
+
- `web-sdk/local-builtin-segments.test.ts`: `chip … with app-set … fails
|
|
453
|
+
closed` (and the delivered cases still match).
|
|
454
|
+
- `web-sdk/customer-side-rt-entitlement-gate.test.ts`: `denies when the app
|
|
455
|
+
sets builtin_dimensions itself via update()`.
|
|
456
|
+
- `web-sdk/customer-side-client-context.test.ts`: `holds exactly the
|
|
457
|
+
delivered value; an app-set value never reaches the context`.
|
|
458
|
+
|
|
459
|
+
### Server SDKs get the control plane's built-in dimensions: `getBuiltinDimensions()` + a server overlay (BL-0366, plan 279 PD-3)
|
|
460
|
+
|
|
461
|
+
**What changed.** Additive. Before this, a server-side decision never saw the
|
|
462
|
+
control plane's Subscription State, Trial Type, Activity State, Seat Type or
|
|
463
|
+
Buyer Role. The Python and Rust ports stamped `rt_*` traits only from the
|
|
464
|
+
app-supplied `builtin_dimensions`, and the fetch that delivers the server's
|
|
465
|
+
values existed only in the browser SDK's client-context lane.
|
|
466
|
+
|
|
467
|
+
- **server-node** — `RevTurbineServer.getBuiltinDimensions(userId)` calls
|
|
468
|
+
`getServerUserBuiltinDimensions`
|
|
469
|
+
(`GET /api/sdk/user-contexts/{userId}/builtin-dimensions`) through the
|
|
470
|
+
generated typed client with the server key the client already holds. It
|
|
471
|
+
resolves to `{ tenant_id, user_id, builtin_dimensions, resolved_at }`
|
|
472
|
+
(re-exported as `ServerUserBuiltinDimensions`). The tenant comes from the
|
|
473
|
+
key: it is not a parameter, and no tenant header is sent. A refusal throws
|
|
474
|
+
the new `RevTurbineBuiltinDimensionsError` (`status`, `requestId`, machine
|
|
475
|
+
`code`, typed `reason`: `invalid_request` / `unauthorized` / `forbidden` /
|
|
476
|
+
`request_failed`). It never carries the key.
|
|
477
|
+
- **The overlay (all three ports).** `createStaticProviders` takes
|
|
478
|
+
`serverBuiltinDimensions` (from `@revt-eng/core`, re-exported by server-node
|
|
479
|
+
together with `applyServerBuiltinDimensions`). Python's
|
|
480
|
+
`create_static_providers(server_builtin_dimensions=…)` and the
|
|
481
|
+
`RevTurbineCustomerSdk` user context key `server_builtin_dimensions` take
|
|
482
|
+
the same object. So do Rust's `StaticProviderOptions::server_builtin_dimensions`
|
|
483
|
+
and `sdk::UserContext::server_builtin_dimensions`, plus the
|
|
484
|
+
`apply_server_builtin_dimensions` helper in both ports. The object is
|
|
485
|
+
overlaid per key on the app-set `builtin_dimensions` before segment
|
|
486
|
+
membership is evaluated. A delivered leaf wins over the app's value (PD-3),
|
|
487
|
+
and an app-set leaf the server did not deliver is kept. This is the overlay
|
|
488
|
+
the browser SDK already applies to a client-context delivery. Without a user
|
|
489
|
+
context it is ignored, because nothing is evaluated then.
|
|
490
|
+
- **Python and Rust get no HTTP client.** Neither port has a transport or a
|
|
491
|
+
server-key client (BL-0354), and this change does not add one. The app's
|
|
492
|
+
backend reads the endpoint itself (or through server-node) and passes the
|
|
493
|
+
response's `builtin_dimensions` in.
|
|
494
|
+
|
|
495
|
+
Rust callers that build `StaticProviderOptions` or `sdk::UserContext` as a
|
|
496
|
+
full struct literal without `..Default::default()` must add the new field.
|
|
497
|
+
|
|
498
|
+
The `@revt-eng/*` pins move to `0.1.373`, which adds the operation and the
|
|
499
|
+
core overlay (scaffold #430).
|
|
500
|
+
|
|
501
|
+
**Landed in:** 0.11.14.
|
|
502
|
+
**Fail-closed in:** n/a (additive).
|
|
503
|
+
**Proving test:** `tests/server-node-get-builtin-dimensions.test.ts`,
|
|
504
|
+
`server-python/tests/adapters/test_static.py`,
|
|
505
|
+
`server-rust/tests/static_providers.rs`, and the parity fixtures
|
|
506
|
+
`segment_membership_server_overlay_wins` / `segment_membership_server_overlay_demotes`.
|
|
507
|
+
|
|
508
|
+
### `RevTurbineProvider` disposes the previous SDK instance on an `options` change (BL-0375)
|
|
509
|
+
|
|
510
|
+
**What changed.** `RevTurbineProvider`'s init effect creates a new SDK instance
|
|
511
|
+
whenever the `options` prop (or `bootstrapPlacements`) changes identity, but its
|
|
512
|
+
cleanup previously only flipped an internal `mounted` flag — it never called
|
|
513
|
+
`dispose()` on the instance it was replacing. A host that rebuilds `options` on
|
|
514
|
+
every render (an unmemoized `options={{ ... }}` literal, or an object built in
|
|
515
|
+
an un-annotated `useMemo` with an unstable dependency — the shape web's dogfood
|
|
516
|
+
provider had until web #944) therefore re-initialized on every render and
|
|
517
|
+
leaked every earlier instance's flush-interval timer, `pagehide` /
|
|
518
|
+
`visibilitychange` listeners, and any buffered telemetry.
|
|
519
|
+
|
|
520
|
+
The cleanup now calls the previous instance's existing `dispose()` (flushes
|
|
521
|
+
pending telemetry, detaches its listeners and timer) before the next render's
|
|
522
|
+
instance is constructed. The re-init contract is unchanged: a new `options`
|
|
523
|
+
object identity — even one with identical content — still creates a new SDK
|
|
524
|
+
instance; that documented behavior was never the bug, the leak was. When a new
|
|
525
|
+
`options` object is recreated with the same content as the previous one, the
|
|
526
|
+
provider now also logs a one-time (per mounted provider lifetime) development
|
|
527
|
+
warning pointing integrators at memoizing `options`, in addition to the
|
|
528
|
+
existing every-time identity-change warning.
|
|
529
|
+
|
|
530
|
+
**Landed in:** no release — no signature changed; `dispose()` on
|
|
531
|
+
`RevTurbineCustomerSdk` already existed (documented for manual SPA-unmount
|
|
532
|
+
teardown) and is now also called automatically by the provider.
|
|
533
|
+
**Fail-closed in:** n/a — purely a leak fix; no customer-visible API changed.
|
|
534
|
+
**Proving test:**
|
|
535
|
+
`web-sdk/react/RevTurbineProvider.dispose-on-options-change.test.tsx`.
|
|
536
|
+
|
|
537
|
+
### Behaviour fix: server SDKs stop serving segment-chipped payloads to every user (BL-0369)
|
|
538
|
+
|
|
539
|
+
**What changed.** On the server lanes, every configured Playbook segment
|
|
540
|
+
counted as a segment the user was in. That covered the Python and Rust
|
|
541
|
+
`RevTurbineCustomerSdk`, `LocalRuntime`, and server-node's
|
|
542
|
+
`LocalEvaluationServer` fed by `createStaticProviders`. So a payload chipped
|
|
543
|
+
to any segment was shown to every user, and an entitlement rule targeted at
|
|
544
|
+
any segment granted to every user on the plan. This included the generated
|
|
545
|
+
built-in segments (`rt.<dimension>.<value>`, e.g.
|
|
546
|
+
`rt.subscription_state.paid`) that hosted Playbooks now export. The browser
|
|
547
|
+
SDK was not affected.
|
|
548
|
+
|
|
549
|
+
A user is now in a segment only when one of these is true, the same rule the
|
|
550
|
+
browser SDK applies:
|
|
551
|
+
|
|
552
|
+
- your app passed its handle in `segment_ids` (Python / Rust) or
|
|
553
|
+
`segmentIds` (`createStaticProviders`), or
|
|
554
|
+
- its predicates match the user's targeting traits. Those traits are built
|
|
555
|
+
from `custom`, the plan, and the reserved `rt_<dimension>` traits.
|
|
556
|
+
`rt_<dimension>` traits come **only** from `builtin_dimensions`, which the
|
|
557
|
+
server sets (plan 279 PD-3). For experiment segments, the user's
|
|
558
|
+
`experiments` assignments decide enrollment.
|
|
559
|
+
|
|
560
|
+
Anything else fails closed. An unset dimension, a trait the user doesn't
|
|
561
|
+
carry, or a chip that names an unknown segment serves nothing (PD-4).
|
|
562
|
+
|
|
563
|
+
New inputs (all additive): `custom`, `builtin_dimensions` and `experiments`
|
|
564
|
+
on the Python `UserContext` and the Rust `UserContext` struct. `segment_ids`
|
|
565
|
+
now also drives chips and rules, not just the catalog helpers.
|
|
566
|
+
`createStaticProviders` gets `segmentIds` and `userContext` from
|
|
567
|
+
`@revt-eng/core`. A side fix: the Python and Rust rules providers now carry
|
|
568
|
+
`segment_dimensions` and the Playbook's `format_version`, like TypeScript.
|
|
569
|
+
Without `segment_dimensions`, a rule targeted at segments in two dimensions
|
|
570
|
+
would have matched on either one once membership became real.
|
|
571
|
+
|
|
572
|
+
**Impact on existing integrations.** This is a correctness fix, but you may
|
|
573
|
+
notice it. If a server-side integration shows segment-chipped payloads or
|
|
574
|
+
grants segment-targeted entitlements, and it passes none of the inputs
|
|
575
|
+
above, those payloads now **stop showing** and those rules **stop
|
|
576
|
+
granting**. They were being over-served before. To keep serving users who
|
|
577
|
+
really are in the segment, pass `segment_ids` for segments your app
|
|
578
|
+
resolves, and `builtin_dimensions` / `custom` for predicate and built-in
|
|
579
|
+
segments. The Rust `UserContext` struct gains three fields. A struct literal
|
|
580
|
+
that does not end in `..Default::default()` needs them added.
|
|
581
|
+
|
|
582
|
+
The `@revt-eng/*` pins move to `0.1.372`: scaffold #429 is the BL-0369 core
|
|
583
|
+
fix, and it carries scaffold #428 (0.1.371: the `billing_health`
|
|
584
|
+
built-in dimension leaf, BL-0317, and `seat_type_handle` becoming
|
|
585
|
+
required-nullable on `ServerUserContextUpsert`, BL-0367). The vendored port
|
|
586
|
+
types follow: in Python, `ServerUserContextUpsert.seat_type_handle` loses its
|
|
587
|
+
default; in Rust, `ClientContextBuiltinDimensionsBillingHealth` is added.
|
|
588
|
+
|
|
589
|
+
**Landed in:** 0.11.14 (`@revt-eng/core` 0.1.372 carries
|
|
590
|
+
the scaffold BL-0369 fix).
|
|
591
|
+
**Fail-closed in:** the same release.
|
|
592
|
+
**Proving test:**
|
|
593
|
+
- `tests/parity/fixtures/segment_membership_{no_inputs,builtin_and_traits,explicit_ids}.json`
|
|
594
|
+
(TS == Python == Rust).
|
|
595
|
+
- `server-python/tests/adapters/test_static.py` and
|
|
596
|
+
`server-rust/tests/static_providers.rs` (segment membership tests).
|
|
597
|
+
- `tests/server-node-local-segment-membership.test.ts`.
|
|
598
|
+
|
|
599
|
+
### `update()` carries `experiments` and `builtin_dimensions`; server-computed keys are dropped at every browser entry point (BL-0352, ruling D-36)
|
|
600
|
+
|
|
601
|
+
**What changed.** `update()`'s exhaustiveness check never ran. It was an
|
|
602
|
+
`expectTypeOf` inside a vitest `.test.ts`, and vitest does not typecheck. Four
|
|
603
|
+
fields had reached `RevTurbineUpdateInput` without reaching
|
|
604
|
+
`RECOGNIZED_UPDATE_KEYS`, so `update()` accepted them at compile time and
|
|
605
|
+
silently dropped them at runtime. Each one now has an explicit disposition:
|
|
606
|
+
|
|
607
|
+
- *(`builtin_dimensions` superseded by BL-0381 above: it is server-assigned
|
|
608
|
+
and dropped.)* `experiments` and `builtin_dimensions` are **now applied**
|
|
609
|
+
by `update()`.
|
|
610
|
+
The app may supply experiment assignments it already knows
|
|
611
|
+
(`runtime/experiment-assignment.md` §3, plan 183), and it may seed built-in
|
|
612
|
+
dimensions, for example in `local_only` mode. The client-context overlay
|
|
613
|
+
still wins when it is present (`targeting-studio-ui.md` §4.1). Before this
|
|
614
|
+
change, `update({ experiments })` compiled and did nothing.
|
|
615
|
+
- `activity_score` and `activity_score_computed_at` are **server-computed**.
|
|
616
|
+
The control plane's hourly activity-score refresh writes them (plan 180),
|
|
617
|
+
and the browser receives only the derived `activity_level` dimension. Like
|
|
618
|
+
`seat_type_handle` (BL-0351), they are now omitted from
|
|
619
|
+
`RevTurbineUserContext`, `RevTurbineUpdateInput` and `IdentifyContextInput`.
|
|
620
|
+
- `setUserContext()` and the `user` init option now **drop server-assigned
|
|
621
|
+
keys at runtime** (`seat_type_handle`, `activity_score`,
|
|
622
|
+
`activity_score_computed_at`) and report them. `identify()` and `update()`
|
|
623
|
+
already dropped them, and now report them with a server-assigned warning
|
|
624
|
+
that names the backend as the writer, not the "pass it under `custom`"
|
|
625
|
+
hint. Before this change, a plain-JS `setUserContext({ seat_type_handle })`
|
|
626
|
+
merged the key into the held context, and local mode persisted it. A state
|
|
627
|
+
blob persisted before this change is purged of those keys when it is
|
|
628
|
+
restored. A server-action result that echoes them back is stripped without
|
|
629
|
+
a warning.
|
|
630
|
+
- New exported types `RecognizedUpdateKey` and `RecognizedIdentifyKey`
|
|
631
|
+
(additive) back a real compile-time derivation in
|
|
632
|
+
`web-sdk/update-keys-exhaustiveness.test-d.ts`, which
|
|
633
|
+
`pnpm check:types:exact` compiles. A future `UserContext` field fails the
|
|
634
|
+
build until it is added to the runtime list or omitted as server-assigned.
|
|
635
|
+
|
|
636
|
+
**Deferred to `0.12.0`:** `setUserContext()` still takes
|
|
637
|
+
`RevTurbineUserContext` without `Exact<>`, so other undeclared keys that
|
|
638
|
+
arrive through a variable are not rejected at compile time. Adding `Exact<>`
|
|
639
|
+
would break callers that pass a wider object today. `IdentifyContextInput`
|
|
640
|
+
also still declares 11 keys that `identify()` drops with a warning (`trial`,
|
|
641
|
+
`tiers`, `experiments`, …). The type-test pins that list, so it cannot grow
|
|
642
|
+
silently. Narrowing the type is a breaking change and waits for the minor.
|
|
643
|
+
|
|
644
|
+
The `@revt-eng/*` pins move to `0.1.370` (scaffold #427: discovery-owned
|
|
645
|
+
`SurfaceSlotSchema.slot_name`). That field now appears in the generated client
|
|
646
|
+
types and the vendored Python and Rust port types. The SDK does not write it.
|
|
647
|
+
|
|
648
|
+
**Landed in:** 0.11.14.
|
|
649
|
+
**Fail-closed in:** the same release. The compile-time omission and the
|
|
650
|
+
runtime drop at all four entry points both ship in it.
|
|
651
|
+
**Proving test:**
|
|
652
|
+
- `web-sdk/update-keys-exhaustiveness.test-d.ts` (`pnpm check:types:exact`).
|
|
653
|
+
- `web-sdk/identify-guardrails.test.ts`: describe blocks
|
|
654
|
+
`server-computed keys never enter the browser context`,
|
|
655
|
+
`setUserContext() drops server-assigned keys at its runtime boundary` and
|
|
656
|
+
`update() carries app-owned experiments` (renamed by BL-0381).
|
|
657
|
+
|
|
658
|
+
### server-node `assignSeatType()` writes a user's seat type (BL-0354, plan 279 TASK-16a, ruling D-36)
|
|
659
|
+
|
|
660
|
+
**What changed.** Additive. `RevTurbineServer` (server-node) gains
|
|
661
|
+
`assignSeatType(userId: string, seatTypeHandle: string | null)`. It calls
|
|
662
|
+
`upsertServerUserContext` (`POST /api/sdk/user-contexts`) through the generated
|
|
663
|
+
typed client with the server key the client already holds. The request body is
|
|
664
|
+
exactly `{ user_id, seat_type_handle }`. `null` clears the assignment. The
|
|
665
|
+
tenant comes from the key: it is not a parameter, and no tenant header is sent.
|
|
666
|
+
It resolves to the stored assignment
|
|
667
|
+
`{ tenant_id, user_id, updated_at, seat_type_handle }` (re-exported as
|
|
668
|
+
`ServerUserContextAssignment`).
|
|
669
|
+
|
|
670
|
+
A refusal throws the new `RevTurbineSeatAssignmentError`. It carries `status`,
|
|
671
|
+
`requestId`, the control plane's machine `code` when present, and a typed
|
|
672
|
+
`reason`: `unknown_seat_type` (422: the handle is not one of the tenant's seat
|
|
673
|
+
types), `forbidden` (403: the credential is not a server key), `unauthorized`
|
|
674
|
+
(401), `invalid_request` (400) or `request_failed`. Like
|
|
675
|
+
`RevTurbineClientSessionError`, it never carries the key or the free-text
|
|
676
|
+
response message.
|
|
677
|
+
|
|
678
|
+
Per D-36 the seat assignment is server-written only, so there is no browser SDK
|
|
679
|
+
surface for this. The Python and Rust ports have no HTTP client or server-key
|
|
680
|
+
client, only the vendored `ServerUserContextUpsert` / `ServerUserContextAssignment`
|
|
681
|
+
models, so they gain no helper.
|
|
682
|
+
|
|
683
|
+
The `@revt-eng/*` pins move to `0.1.369`, which adds that operation to the
|
|
684
|
+
external contract (scaffold #425).
|
|
685
|
+
|
|
686
|
+
**Landed in:** 0.11.14.
|
|
687
|
+
**Fail-closed in:** n/a (additive).
|
|
688
|
+
**Proving test:** `tests/server-node-assign-seat-type.test.ts`.
|
|
689
|
+
|
|
690
|
+
### `seat_type_handle` is server-assigned and never a browser input (BL-0351, plan 279 TASK-16a, ruling D-36)
|
|
691
|
+
|
|
692
|
+
**What changed.** Scaffold `0.1.368` adds `UserContextSchema.seat_type_handle`,
|
|
693
|
+
the user's persisted seat-type assignment. Per D-36 it has exactly two writers:
|
|
694
|
+
the tenant's backend (a server-key user-context upsert) and RevTurbine's own
|
|
695
|
+
server-side enrichment in hosted mode. The browser never writes it. Without a
|
|
696
|
+
change here the new field would have flowed into the browser input types
|
|
697
|
+
through `UserContextInput`. Instead:
|
|
698
|
+
|
|
699
|
+
- `RevTurbineUserContext` omits `seat_type_handle`. So do
|
|
700
|
+
`RevTurbineUpdateInput`, which derives from it, and `IdentifyContextInput`.
|
|
701
|
+
`identify()`, `update()` and the `user` init option reject the key at compile
|
|
702
|
+
time, including when it arrives through a variable (`Exact<…>`).
|
|
703
|
+
`setUserContext()` rejects it in an object literal.
|
|
704
|
+
- The key stays out of the runtime recognized-key lists, so a plain-JS caller's
|
|
705
|
+
`seat_type_handle` is dropped at the `identify()`/`update()` boundary and
|
|
706
|
+
reported by the existing unrecognized-key warning. It is never merged into the
|
|
707
|
+
held context.
|
|
708
|
+
- The browser receives the **resolved** seat type as
|
|
709
|
+
`builtin_dimensions.seat_type` / `builtin_dimensions.buyer_role` from the
|
|
710
|
+
client-context fetch (plan 279 TASK-16, web), unchanged here.
|
|
711
|
+
|
|
712
|
+
No released SDK type ever carried `seat_type_handle`, so nothing an
|
|
713
|
+
integration compiles today stops compiling. The `@revt-eng/*` pins move to
|
|
714
|
+
`0.1.368`. That version also adds `SeatTypeSchema.is_buyer` (default `false`),
|
|
715
|
+
which now appears in the vendored Python and Rust port types.
|
|
716
|
+
|
|
717
|
+
A server-node `assignSeatType(userId, handle | null)` helper was **not** in this
|
|
718
|
+
change, because the server-key operation it wraps (`upsertServerUserContext`,
|
|
719
|
+
`POST /api/sdk/user-contexts`, plan 279 TASK-15a) was not yet in the published
|
|
720
|
+
external contract. It shipped with scaffold `0.1.369` (see the BL-0354 entry
|
|
721
|
+
above).
|
|
722
|
+
|
|
723
|
+
**Landed in:** 0.11.14. This PR bumps pins only.
|
|
724
|
+
**Fail-closed in:** the same release (compile-time rejection; runtime drop).
|
|
725
|
+
**Proving test:**
|
|
726
|
+
- `web-sdk/user-context-exactness.test-d.ts`: the `@ts-expect-error` block
|
|
727
|
+
"Server-assigned fields" (`pnpm check:types:exact`).
|
|
728
|
+
- `web-sdk/identify-guardrails.test.ts`: describe block
|
|
729
|
+
`seat_type_handle never enters the browser context`.
|
|
730
|
+
|
|
50
731
|
## 0.11.13
|
|
51
732
|
|
|
52
733
|
### Slot lifecycle events carry the slot's `route` (BL-0207, ruling D-30)
|