@revturbine/sdk 0.11.19 → 0.11.21

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,78 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.11.21
51
+
52
+ ### `getPlacement` is a pure function of (user context, Playbook) (D-39, BL-0379)
53
+
54
+ **What changed.** Behaviour change in the browser SDK; no signature changes.
55
+
56
+ - **One decision path.** `getPlacement(config)` resolves the Playbook's
57
+ authored slot through the same decision path as `getPlacementDecision` and
58
+ the server SDKs' `get_placement` / `LocalRuntime.getPlacement`. 0.11.20 first
59
+ consulted a lane of previously decided or seeded outputs and ranked them with
60
+ a separate selection layer (category bucket, urgency, score), so it could
61
+ return a different placement than the decision path for the same user.
62
+ - **Always current.** Each call reflects the current user context; the only
63
+ caching is the decision path's own memo keyed by the request and context.
64
+ - **`getPlacementContent`** in local-only mode now decides through the same
65
+ path instead of reading seeded outputs.
66
+ - **Deprecated, ignored:** `localRuntime.initialData.placementsByLookupKey`
67
+ (warns once when supplied). Author placements in `localRuntime.playbook`, or
68
+ implement the `getPlacement` resolver for a custom source. The field, and
69
+ core's `resolveLocalPlacementFromCandidates` selection helper, are removed in
70
+ 0.12.0 (BL-0167).
71
+ - **Live design partner:** an integration that seeded placement outputs, or
72
+ relied on the old cached ranking, now sees the Playbook's decision.
73
+
74
+ **Landed in** 0.11.21. **Fail-closed in** 0.11.21.
75
+
76
+ **Proving tests.** `web-sdk/pure-getplacement.test.ts` (browser `getPlacement`
77
+ equals the server runtime for the same inputs; reflects a changed user
78
+ context; ignores seeded outputs); `web-sdk/customer-side-fixed-only.test.ts`.
79
+
80
+ ## 0.11.20
81
+
82
+ ### One entitlement answer for browser and server, with Entitlement Mirroring; Access Gates only fire while denied (D-60, D-61)
83
+
84
+ **What changed.** Behaviour change in every port. Browser and server now run
85
+ the same evaluation on the same inputs: the browser is trusted for
86
+ interaction, and the server verifies.
87
+
88
+ - **One effective entitlement (ts/py/rs/web).** Every entitlement check runs
89
+ the shared core evaluator over the Playbook and the user context (plan,
90
+ segments, usage, reverse-trial grants), then merges app-supplied data. Server
91
+ runtimes previously used a separate rule path that ignored reverse-trial
92
+ grants and allowed everything when the Playbook had no entitlement rules.
93
+ **Server-side results change for the live design partner:** a configured
94
+ entitlement with no rule for the user's plan is now denied on the server
95
+ too, and reverse-trial grants are honoured there.
96
+ - **Entitlement Mirroring.** `UserContext.entitlements` accepts a boolean or a
97
+ grant (`status`, `limit`, `used`), and an app entitlement provider's
98
+ entries count as app data. By default the app's data wins;
99
+ `entitlementMerge` (`entitlement_merge` in Python and Rust) sets provider-level
100
+ `precedence` and per-field overrides. Browser-supplied data shapes the UI;
101
+ the server re-check stays authoritative.
102
+ - **Unknown entitlement handles fail closed.** A handle neither the Playbook
103
+ nor the app knows is denied with `entitlement_not_in_playbook`, warned once,
104
+ and reported (browser telemetry; `onUnknownEntitlement` on the server).
105
+ 0.11.19 returned `entitlement_not_found_default_allow`.
106
+ - **Access Gates (D-60).** A gate placement appears only in an Access Gate
107
+ slot and fires only while its entitlement is denied or limited (a
108
+ tier-scoped gate below its tier counts as limited). Placement gates read the
109
+ same effective entitlements `checkEntitlement` returns.
110
+ - **Reason codes.** New entitlement reason `entitlement_mirrored`; new
111
+ placement reasons `entitlement_not_denied` and `gate_outside_access_gate`.
112
+ - **Scaffold pin 0.1.402 → 0.1.409** (revturbine-scaffold #462, #465
113
+ and #468; vendored `types.py` / `types.rs` refreshed).
114
+
115
+ **Landed in** 0.11.20. **Fail-closed in** 0.11.20.
116
+
117
+ **Proving tests.** `web-sdk/entitlement-mirroring.test.ts`; the D-60/D-61
118
+ blocks in each port's resolver and runtime tests; parity fixtures
119
+ `placement_slot_selection_is_category_first` and
120
+ `placement_category_interactions`.
121
+
50
122
  ## 0.11.19
51
123
 
52
124
  ### Placement decisions rank category first; caps run before the pick; overall presentation cap rules enforced (D-59, BL-0536)
@@ -293,6 +293,16 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
293
293
  [key: string]: JsonValue;
294
294
  };
295
295
 
296
+ /** A mirrored grant on `UserContext.entitlements`: a boolean, or a grant-shaped record. */
297
+ type MirroredEntitlement = boolean | {
298
+ status?: string;
299
+ allowed?: boolean;
300
+ limit?: number;
301
+ used?: number;
302
+ remaining?: number;
303
+ reason?: string;
304
+ };
305
+
296
306
  /**
297
307
  * User context lifecycle — pure functions for building, merging,
298
308
  * and deriving targeting state from user context + RevTurbineConfig.
@@ -315,7 +325,12 @@ interface UserContextSnapshot {
315
325
  */
316
326
  plan?: string | UserPlanContext;
317
327
  custom?: Record<string, JsonValue>;
318
- entitlements?: Record<string, boolean>;
328
+ /**
329
+ * App-supplied entitlement data keyed by handle — a boolean or a grant
330
+ * (D-61 Entitlement Mirroring). Merged with the Playbook evaluation by the
331
+ * effective-entitlement core; flattened into segment traits as a boolean.
332
+ */
333
+ entitlements?: Record<string, MirroredEntitlement>;
319
334
  usage?: Record<string, UserUsageEntry>;
320
335
  personalization?: Record<string, JsonValue>;
321
336
  /** Canonical experiment-handle to variant-handle assignments. */
@@ -27720,6 +27720,52 @@ type PlaybookOption = {
27720
27720
  exportedConfig: RevTurbineConfig;
27721
27721
  };
27722
27722
 
27723
+ /**
27724
+ * Effective entitlements — the ONE answer every SDK runtime uses (D-61,
27725
+ * Kent 2026-10-06).
27726
+ *
27727
+ * The shared evaluator (`deriveLocalEntitlementFromConfiguredRules`) decides
27728
+ * each entitlement from the Playbook + user context. An app may then enrich
27729
+ * that answer with its own data ("Entitlement Mirroring"): grants on
27730
+ * `UserContext.entitlements` and/or an app-registered entitlement provider.
27731
+ * The two are merged with a configurable precedence — provider level
27732
+ * (`precedence`, default `'app'`) with optional per-field overrides.
27733
+ *
27734
+ * Browser and server SDKs run this same function on the same inputs: the
27735
+ * browser result shapes interaction, the server result verifies it.
27736
+ *
27737
+ * An entitlement handle neither the Playbook nor the app knows is DENIED
27738
+ * (`entitlement_not_in_playbook`) and flagged `unknownHandle`, so the runtime
27739
+ * can warn and report it.
27740
+ */
27741
+
27742
+ /** Which side wins a merge: the app's own data, or the Playbook evaluation. */
27743
+ type EntitlementSource = 'app' | 'playbook';
27744
+ /** Fields that can be given their own precedence. `status` carries `allowed` and `reason` with it. */
27745
+ type EntitlementMergeField = 'status' | 'limit' | 'used' | 'remaining';
27746
+ interface EntitlementMergeOptions {
27747
+ /** Provider-level precedence. Default `'app'`: app-supplied data wins. */
27748
+ precedence?: EntitlementSource;
27749
+ /** Per-field overrides of `precedence`. */
27750
+ fields?: Partial<Record<EntitlementMergeField, EntitlementSource>>;
27751
+ }
27752
+ /** A mirrored grant on `UserContext.entitlements`: a boolean, or a grant-shaped record. */
27753
+ type MirroredEntitlement = boolean | {
27754
+ status?: string;
27755
+ allowed?: boolean;
27756
+ limit?: number;
27757
+ used?: number;
27758
+ remaining?: number;
27759
+ reason?: string;
27760
+ };
27761
+ /** The trial facts reverse-trial grants depend on (`UserTrialStatus` subset). */
27762
+ interface ReverseTrialView {
27763
+ in_trial?: boolean;
27764
+ trial_type?: string;
27765
+ /** The user's BASE plan while on a reverse trial. */
27766
+ plan_handle?: string;
27767
+ }
27768
+
27723
27769
  /**
27724
27770
  * User context lifecycle — pure functions for building, merging,
27725
27771
  * and deriving targeting state from user context + RevTurbineConfig.
@@ -27742,7 +27788,12 @@ interface UserContextSnapshot {
27742
27788
  */
27743
27789
  plan?: string | UserPlanContext;
27744
27790
  custom?: Record<string, JsonValue>;
27745
- entitlements?: Record<string, boolean>;
27791
+ /**
27792
+ * App-supplied entitlement data keyed by handle — a boolean or a grant
27793
+ * (D-61 Entitlement Mirroring). Merged with the Playbook evaluation by the
27794
+ * effective-entitlement core; flattened into segment traits as a boolean.
27795
+ */
27796
+ entitlements?: Record<string, MirroredEntitlement>;
27746
27797
  usage?: Record<string, UserUsageEntry>;
27747
27798
  personalization?: Record<string, JsonValue>;
27748
27799
  /** Canonical experiment-handle to variant-handle assignments. */
@@ -28023,6 +28074,13 @@ interface EntitlementUsageEntry {
28023
28074
  reset_date?: string;
28024
28075
  }
28025
28076
  interface EntitlementProviderState {
28077
+ /**
28078
+ * D-61: `'playbook_default'` marks entries the static adapter fills with a
28079
+ * blanket default policy. Those are NOT app data and never override the
28080
+ * Playbook evaluation; entries from any other provider are app-mirrored
28081
+ * data and merge with it per the runtime's precedence.
28082
+ */
28083
+ origin?: 'playbook_default';
28026
28084
  /**
28027
28085
  * All entitlement results keyed by entitlement handle.
28028
28086
  * Each entry carries status ('allowed' | 'limited' | 'denied') so the
@@ -28496,7 +28554,7 @@ interface DecisionEngineOptions {
28496
28554
  enableCapsEnforcement?: boolean;
28497
28555
  /** Enable category pipeline for local-mode priority ordering. Default: false. */
28498
28556
  enableCategoryPipelineLocalMode?: boolean;
28499
- /** Default entitlement policy when no provider or entry found. Default: 'allow'. */
28557
+ /** Default entitlement policy when no provider or entry found. Default: 'deny' (fail closed, D-61). */
28500
28558
  defaultEntitlementPolicy?: 'allow' | 'deny';
28501
28559
  }
28502
28560
  interface EntitlementCheckResult {
@@ -29346,6 +29404,20 @@ interface LocalRuntimeOptionsBase {
29346
29404
  impressionStore?: ImpressionHistoryStore;
29347
29405
  /** Decision engine behavior flags. */
29348
29406
  engineOptions?: DecisionEngineOptions;
29407
+ /**
29408
+ * D-61 Entitlement Mirroring: the user context's own entitlement data
29409
+ * (`UserContext.entitlements`), merged with the Playbook evaluation.
29410
+ */
29411
+ userEntitlements?: Record<string, MirroredEntitlement>;
29412
+ /** D-61: the user's trial status, so reverse-trial grants evaluate exactly as in the browser. */
29413
+ trialStatus?: ReverseTrialView;
29414
+ /** D-61: how app-supplied entitlement data merges with the Playbook result. Default: app wins. */
29415
+ entitlementMerge?: EntitlementMergeOptions;
29416
+ /**
29417
+ * Called once per entitlement handle that neither the Playbook nor the app
29418
+ * knows (it is denied). The runtime also logs a warning. Wire telemetry here.
29419
+ */
29420
+ onUnknownEntitlement?: (handle: string) => void;
29349
29421
  /** Interaction tracker options. */
29350
29422
  interactionOptions?: Partial<Pick<InteractionTrackerOptions, 'defaultDismissCooldownMs' | 'defaultRemindLaterMs'>>;
29351
29423
  }
@@ -29366,6 +29438,11 @@ declare class LocalRuntime {
29366
29438
  readonly impressionHistory: ImpressionHistory;
29367
29439
  private readonly playbook;
29368
29440
  private readonly registeredPlacements;
29441
+ private readonly userEntitlements;
29442
+ private readonly trialStatus;
29443
+ private readonly entitlementMerge;
29444
+ private readonly onUnknownEntitlement;
29445
+ private readonly reportedUnknownEntitlements;
29369
29446
  private userId;
29370
29447
  constructor(options: LocalRuntimeOptions);
29371
29448
  /**
@@ -29403,7 +29480,12 @@ declare class LocalRuntime {
29403
29480
  */
29404
29481
  getPlacement(config: RevTurbinePlacementRequestConfig$1): Promise<RevTurbinePlacementDecision | null>;
29405
29482
  /**
29406
- * Check entitlement access locally using RevTurbineConfig rules.
29483
+ * Check entitlement access locally (D-61): the shared evaluator decides
29484
+ * from the Playbook + user context, then app-mirrored data (user-context
29485
+ * grants and any app entitlement provider) merges per the runtime's
29486
+ * precedence (default: app wins). The browser SDK runs the same function on
29487
+ * the same inputs, so the server verifies exactly what the browser showed.
29488
+ * An entitlement nobody knows is denied, warned and reported.
29407
29489
  */
29408
29490
  checkEntitlement(handle: string, context?: RevTurbineEntitlementContext): Promise<EntitlementResult>;
29409
29491
  /**
@@ -29466,7 +29548,11 @@ declare class LocalRuntime {
29466
29548
  */
29467
29549
  private slotRecordForConfig;
29468
29550
  private buildPlacementResolver;
29469
- private deriveEntitlementFromConfig;
29551
+ /** Evaluator inputs from the resolved providers — the same facts the browser SDK uses. */
29552
+ private effectiveBase;
29553
+ /** App-mirrored data per handle: user-context grants plus non-default provider entries. */
29554
+ private appInputsByHandle;
29555
+ private reportUnknownEntitlement;
29470
29556
  }
29471
29557
 
29472
29558
  /**
@@ -30817,8 +30903,14 @@ interface RevTurbineUserContext extends Omit<UserContextInput, 'custom' | 'entit
30817
30903
  context_hash?: UserContextInput['context_hash'];
30818
30904
  /** Customer-defined fields for segmentation and personalization. */
30819
30905
  custom?: SdkTraits;
30820
- /** Feature entitlements granted by plan + entitlement rules. */
30821
- entitlements?: Record<string, boolean>;
30906
+ /**
30907
+ * App-supplied entitlement data, keyed by handle — a boolean or a grant
30908
+ * (`status`, `limit`, `used`). This is Entitlement Mirroring (D-61): it
30909
+ * merges with the Playbook evaluation, and by default the app's data wins.
30910
+ * Browser-supplied data shapes the UI only; the server re-check on the
30911
+ * same inputs stays authoritative for paid actions.
30912
+ */
30913
+ entitlements?: UserContextInput['entitlements'];
30822
30914
  /** Usage entries derived from credits / usage_limit entitlements, keyed by handle. */
30823
30915
  usage?: UserContextInput['usage'];
30824
30916
  /**
@@ -31301,6 +31393,14 @@ interface RevTurbineInitOptions {
31301
31393
  * Playbook.
31302
31394
  */
31303
31395
  placementBehavior?: Partial<RevTurbinePlacementBehaviorFlags>;
31396
+ /**
31397
+ * How app-supplied entitlement data (`userContext.entitlements`, an app
31398
+ * entitlement provider) merges with the Playbook evaluation (D-61).
31399
+ * Provider-level `precedence` defaults to `'app'`; `fields` overrides it
31400
+ * per field (`status`, `limit`, `used`, `remaining`). Use the same value
31401
+ * on your server SDK so verification matches.
31402
+ */
31403
+ entitlementMerge?: EntitlementMergeOptions;
31304
31404
  extension?: {
31305
31405
  enabled?: boolean;
31306
31406
  };
@@ -31422,6 +31522,11 @@ interface RevTurbineEventBatchingOptions {
31422
31522
  }
31423
31523
  interface RevTurbineLocalRuntimeData {
31424
31524
  placementDecisionsByPlacementId?: Record<string, RevTurbinePlacementDecision>;
31525
+ /**
31526
+ * @deprecated Ignored since 0.11.21 (D-39 / BL-0379): `getPlacement` is a
31527
+ * pure function of the user context and `localRuntime.playbook`, so
31528
+ * pre-decided outputs are never consulted. Removed in 0.12.0.
31529
+ */
31425
31530
  placementsByLookupKey?: Record<string, PlacementOutput | null>;
31426
31531
  entitlementByHandle?: Record<string, EntitlementResult>;
31427
31532
  userContextByUserId?: Record<string, UserTargetingContext>;
@@ -32075,6 +32180,16 @@ declare class RevTurbineCustomerSdk {
32075
32180
  private readonly policy;
32076
32181
  private readonly extensionEnabled;
32077
32182
  private readonly placementBehaviorOverrides;
32183
+ private readonly entitlementMerge;
32184
+ /** Entries from an app-registered entitlement provider (D-61 mirroring), from the last provider resolution. */
32185
+ private appProviderEntitlements;
32186
+ /** Plan from an app-registered plan provider (D-61) — wins over the user-context plan, as on the server. */
32187
+ private appProviderPlanHandle;
32188
+ /** Usage from an app entitlement provider (D-61), fed into the evaluation as on the server. */
32189
+ private appProviderUsage;
32190
+ /** Segment membership from an app segment provider (D-61), as a server runtime sees it. */
32191
+ private appProviderSegments;
32192
+ private readonly reportedUnknownEntitlements;
32078
32193
  private derivedPlacementBehaviorMemo;
32079
32194
  private readonly providerFailureSlotBehavior;
32080
32195
  private readonly uiPathResolvers;
@@ -32189,7 +32304,6 @@ declare class RevTurbineCustomerSdk {
32189
32304
  private pendingConversionPlan?;
32190
32305
  private usageBalances;
32191
32306
  private localDecisionsByPlacementId;
32192
- private localPlacementsByLookupKey;
32193
32307
  private localEntitlementsByHandle;
32194
32308
  private localUserContextsByUserId;
32195
32309
  /**
@@ -32490,12 +32604,6 @@ declare class RevTurbineCustomerSdk {
32490
32604
  */
32491
32605
  private hasEndpointOverride;
32492
32606
  private endpointFor;
32493
- private localPlacementLookupKey;
32494
- private localLookupMatchesConfig;
32495
- private localOutputMatchesConfig;
32496
- private applyCategoryConflictSuppression;
32497
- private resolveLocalPlacementFromCandidates;
32498
- private localPlacementForConfig;
32499
32607
  /**
32500
32608
  * The `placement_slots[]` entry the loaded Playbook authors for a request.
32501
32609
  *
@@ -32573,6 +32681,14 @@ declare class RevTurbineCustomerSdk {
32573
32681
  */
32574
32682
  private candidateCapGate;
32575
32683
  private recordPresentationCapRules;
32684
+ /**
32685
+ * D-60 + D-61: the resolver's gates read the SAME effective entitlements
32686
+ * `checkEntitlement` returns — the shared evaluation merged with
32687
+ * app-mirrored data — for every handle the Playbook, the user context or an
32688
+ * app provider knows. A gate slot's reported `entitlement_status` still
32689
+ * wins inside the resolver.
32690
+ */
32691
+ private withSlotEntitlementStatus;
32576
32692
  private slotIdForRecord;
32577
32693
  private hydratePresentationCaps;
32578
32694
  private persistPresentationCaps;
@@ -32749,22 +32865,25 @@ declare class RevTurbineCustomerSdk {
32749
32865
  * @public
32750
32866
  */
32751
32867
  setApiBranding(branding: BrandingConfig | undefined): void;
32868
+ /**
32869
+ * The effective entitlement (D-61): the shared core evaluator decides from
32870
+ * the Playbook + user context, then app-mirrored data merges per
32871
+ * `entitlementMerge` (default: app wins). The server SDKs run the same
32872
+ * function on the same inputs, so a server re-check verifies exactly this.
32873
+ * An entitlement nobody knows is denied, warned once and reported.
32874
+ */
32752
32875
  private deriveLocalEntitlementFromConfiguredRules;
32753
32876
  /**
32754
- * Inline adapter — given a UserTrialStatus + the tenant's
32755
- * reverse_trial_rules from RevTurbineConfig, derive the inputs that
32756
- * close plan 43 TASK-2. Returns undefined fields when the user
32757
- * isn't on an active reverse trial, when no rule matches, or when
32758
- * the rule has no entitlements_during_trial[].
32759
- *
32760
- * Why inline (not calling scaffold's deriveReverseTrialGrants):
32761
- * the scaffold helper takes a `TrialInstance` (DB-side state) and
32762
- * matches by `rule_id`. The SDK only holds `UserTrialStatus` (the
32763
- * transient runtime shape) and doesn't see the rule_id. We match
32764
- * by `fallback_plan_id === plan_handle` instead — that's the
32765
- * single configured rule the user can be on per spec §2.4.2.
32877
+ * D-61: snapshot what the app's own providers say — plan, entitlement
32878
+ * entries, usage — so the browser evaluates on exactly the inputs a server
32879
+ * runtime with the same providers would. A static adapter's blanket
32880
+ * defaults are not app data.
32766
32881
  */
32767
- private resolveReverseTrialGrants;
32882
+ private captureAppProviderState;
32883
+ /** D-61: resolve the app's providers before an entitlement check (cached per provider TTL). */
32884
+ private refreshAppProviderState;
32885
+ /** D-61: an unknown entitlement handle is denied; say so once, locally and in telemetry. */
32886
+ private reportUnknownEntitlement;
32768
32887
  /**
32769
32888
  * Return the SDK-resolved targeting snapshot for the active user.
32770
32889
  *