@revturbine/sdk 0.10.6 → 0.10.8

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
@@ -47,6 +47,105 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.10.8
51
+
52
+ ### `{{recommended_plan_name}}` and `{{plan_name}}` no longer reach end users as raw tokens (BL-0121)
53
+
54
+ **What changed.** The placement-decision lane now substitutes the five tokens
55
+ the SDK resolves from the Playbook — `plan_name`, `plan_price`,
56
+ `upgrade_plan_price`, `recommended_plan_handle`, `recommended_plan_name` —
57
+ into `RevTurbinePlacementDecision.content` (and the mirrored
58
+ `decision.output.content`). It previously substituted `plan_price` and
59
+ `upgrade_plan_price` only.
60
+
61
+ `recommended_plan_handle` / `recommended_plan_name` were already derived, by
62
+ the parity-locked `resolveRecommendedPlanTokens` dispatch, and already readable
63
+ via `getPersonalizationTokens()`. Nothing wrote them into rendered content, so
64
+ a placement authored with `"Upgrade to {{recommended_plan_name}}"` shipped the
65
+ literal braces to the end user. Observed on the CybeDefend demo, 2026-09-22.
66
+
67
+ Two properties of the substitution are deliberate:
68
+
69
+ | input | result |
70
+ |---|---|
71
+ | a token the SDK owns but cannot resolve (top-of-ladder user has no next plan) | empty string — the spec's documented empty-token convention |
72
+ | a token the SDK does not own (`{{usage_percent}}`, an app-defined token) | left verbatim, so the React render lane still resolves it |
73
+
74
+ `plan_name` resolves through the plan's `unique_handle`, the matching identity
75
+ — the lookup used to be handed the plan *object*, which by design resolves
76
+ nothing, so `{{plan_name}}` rendered raw for every user identified by
77
+ `plan_handle`.
78
+
79
+ **Landed in.** `0.10.8`.
80
+
81
+ **Not fixed here.** `{{upgrade_plan_price}}` still renders the ANNUAL amount
82
+ for a plan that has both an annual and a monthly variation. Variation
83
+ preference keys off the user's billing cadence, but no supported input carries
84
+ it (`billing_period` is absent from `UserContextSchema` and from the recognized
85
+ `identify()` / `update()` keys), so selection falls through to the
86
+ alphabetically-first variation and `<plan>_annual` always wins. Closing it
87
+ needs a schema field for the cadence and a contract for which period the token
88
+ reflects when the cadence is unknown; both are open on BL-0121.
89
+
90
+ **Proving test.** `web-sdk/customer-side-upgrade-tokens.test.ts`.
91
+
92
+ ---
93
+
94
+ ## 0.10.7
95
+
96
+ ### The Python and Rust ports stop filtering payloads on `status` (BL-0151)
97
+
98
+ **What changed.** Payload `status` is no longer a runtime predicate in any port.
99
+ Python and Rust required `status == "active"` on a placement payload before it
100
+ could become a candidate, and read a `status` off each content-linked studio
101
+ payload before overlaying its copy. Both now ignore it, matching the TypeScript
102
+ port, which never gated on it.
103
+
104
+ | | Before | After |
105
+ |---|---|---|
106
+ | Placement payload candidacy (Python, Rust) | required `status == "active"` | every payload, status ignored |
107
+ | Content-linked copy overlay (Python, Rust) | required the studio payload's `status` to read `active` | always overlaid |
108
+ | TypeScript | already ignored `status` | unchanged |
109
+
110
+ **Why it mattered.** `status` is not a field of the exported Playbook.
111
+ `RevTurbineConfigStudioPayload` and `RevTurbineConfigPlacementPayloadItem` carry
112
+ no `status` key, unknown keys are stripped on parse, and the published JSON
113
+ Schema is `additionalProperties: false` — because runtime status is *derived*
114
+ control-plane side and the stored column was dropped. So `status == "active"` was
115
+ false for **every** payload of a real Playbook: the Python and Rust resolvers
116
+ indexed no candidates at all and returned `placement_not_found`, and their
117
+ content-lookup adapters dropped every content-linked payload, keeping the inline
118
+ copy where TypeScript overlaid the linked block. A config whose first payload
119
+ carried a stray authored `status` also decided differently across ports.
120
+
121
+ The cross-language parity gate could not see any of this: the only fixtures
122
+ reaching these paths hand-write `status` on each payload — a key the schema would
123
+ strip — so the ports were compared on a shape no Playbook has.
124
+
125
+ **Landed in.** 0.10.7 (`server-python`, `server-rust`). No `@revt-eng/core`
126
+ change rides with it: TypeScript was already correct, so there is no scaffold
127
+ release and no pin bump.
128
+
129
+ **Fail-closed in.** n/a — this release removes a fail-*closed* filter. Python and
130
+ Rust consumers on a real Playbook go from no decision to the decision
131
+ TypeScript has always returned.
132
+
133
+ **Proving test.** `tests/parity/fixtures/payload_status_filter.json` — two lanes:
134
+ a stray authored `status` that disagrees with drag order (payload 1 `draft`,
135
+ payload 2 `active`; payload 1 must still win), and the real wire shape with no
136
+ `status` anywhere plus a content-linked overlay. Mutation-checked by reinstating
137
+ the Python filter, which diverges on the first call. Plus
138
+ `server-python/tests/placements/test_local_resolver.py`,
139
+ `test_json_content_provider.py` and `server-rust/tests/static_resolver.rs`, each
140
+ covering both the stray-value and the no-key case.
141
+
142
+ **Who is affected.** Every `revturbine` (PyPI) and `revturbine` (crates.io)
143
+ consumer resolving placements from a schema-valid exported Playbook — that is,
144
+ one produced by the control plane rather than hand-written with a `status` field.
145
+ Those integrations were receiving no placement decisions at all.
146
+
147
+ ---
148
+
50
149
  ## 0.10.6
51
150
 
52
151
  ### The trial-status PlanProvider overlay ships in the Rust crate (BL-0153)
@@ -21140,6 +21140,29 @@ declare class RevTurbineCustomerSdk {
21140
21140
  getEligibleAddons(): Promise<EligibleAddon[]>;
21141
21141
  private formatEligiblePrice;
21142
21142
  private priceTokensForProviders;
21143
+ /**
21144
+ * The plan-and-recommendation token family the SDK resolves authoritatively
21145
+ * from the Playbook, for substitution into decision content.
21146
+ *
21147
+ * These five are exactly the tokens whose values come from the Playbook's
21148
+ * plan catalogue plus the recommendation strategy, so the SDK — not the
21149
+ * host app — is the only thing that can fill them in. Every other token in
21150
+ * the studio table (usage, trial, seats, credits) is resolved on the React
21151
+ * render lane by `derivePlacementPersonalizationTokens`, which unknown
21152
+ * tokens pass through untouched.
21153
+ */
21154
+ private derivedContentTokens;
21155
+ /**
21156
+ * Substitute the SDK-resolved plan/price/recommendation tokens into a
21157
+ * decision's content.
21158
+ *
21159
+ * Only the tokens in {@link derivedContentTokens} are substituted; any
21160
+ * other `{{token}}` is left verbatim so the render lane (or the host app)
21161
+ * can still resolve it. A token the SDK owns but cannot resolve — a
21162
+ * top-of-ladder user has no recommended plan — becomes an empty string,
21163
+ * which is the spec's documented empty-token convention and is what keeps
21164
+ * raw `{{...}}` braces off the end user's screen.
21165
+ */
21143
21166
  private applyPriceTokens;
21144
21167
  /**
21145
21168
  * Return current SDK policy snapshot.