@everfur/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,10 +4,23 @@ All notable changes to `@everfur/sdk`.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions are semver.
6
6
 
7
- This is the first release. The line is deliberately pre-1.0: the API is stable enough to build against and
7
+ The line is deliberately pre-1.0: the API is stable enough to build against and
8
8
  is guarded by api-extractor golden reports across all eleven subpaths, but 1.0.0 is a promise about breaking
9
9
  changes that has not been earned by any production integration yet.
10
10
 
11
+ ## [0.1.1] - 2026-09-03
12
+
13
+ ### Fixed — privacy
14
+
15
+ - **An anonymous logout left mounted surfaces on the previous visitor's session.** `logout()` from an
16
+ already-anonymous state compared `userRef` to `userRef`, saw null-to-null, and took its same-identity
17
+ path: the session id rotated but surfaces already mounted (chat, records, photo, video) kept rendering
18
+ against the scope they were mounted with. On a shared or kiosk device the next visitor could see the
19
+ previous one's conversation. Every mounted surface is now rescoped on logout regardless of whether the
20
+ identity appears to have changed.
21
+
22
+ Shipped in 0.1.0. Anyone on 0.1.0 should upgrade.
23
+
11
24
  ## [0.1.0] - 2026-08-23
12
25
 
13
26
  ### Fixed — correctness
@@ -456,7 +456,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
456
456
  suggestedPrompts: Object.freeze([])
457
457
  });
458
458
  function useEverfurChat(opts) {
459
- const { runtime, userRef, activePet } = useEverfurContext();
459
+ const { runtime, userRef, activePet, scopeEpoch } = useEverfurContext();
460
460
  const petRef = opts?.petRef ?? activePet ?? null;
461
461
  const conversationId = opts?.conversationId ?? null;
462
462
  const [controller, setController] = react.useState(null);
@@ -467,7 +467,7 @@ function useEverfurChat(opts) {
467
467
  setController((current) => current === acquired ? null : current);
468
468
  runtime.registry.releaseChat(acquired);
469
469
  };
470
- }, [runtime, userRef, petRef, conversationId]);
470
+ }, [runtime, userRef, petRef, conversationId, scopeEpoch]);
471
471
  const subscribe = react.useCallback(
472
472
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
473
473
  [controller]
@@ -454,7 +454,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
454
454
  suggestedPrompts: Object.freeze([])
455
455
  });
456
456
  function useEverfurChat(opts) {
457
- const { runtime, userRef, activePet } = useEverfurContext();
457
+ const { runtime, userRef, activePet, scopeEpoch } = useEverfurContext();
458
458
  const petRef = opts?.petRef ?? activePet ?? null;
459
459
  const conversationId = opts?.conversationId ?? null;
460
460
  const [controller, setController] = useState(null);
@@ -465,7 +465,7 @@ function useEverfurChat(opts) {
465
465
  setController((current) => current === acquired ? null : current);
466
466
  runtime.registry.releaseChat(acquired);
467
467
  };
468
- }, [runtime, userRef, petRef, conversationId]);
468
+ }, [runtime, userRef, petRef, conversationId, scopeEpoch]);
469
469
  const subscribe = useCallback(
470
470
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
471
471
  [controller]
@@ -2878,7 +2878,7 @@ var EverfurRuntimeImpl = class {
2878
2878
  this.uploadTransport = cfg.uploadTransport ?? unavailableUploadTransport;
2879
2879
  this.imageCache = cfg.imageCache ?? noopImageCache;
2880
2880
  this.kv = cfg.storage ?? createInMemoryKv();
2881
- this.currentUser = input.user;
2881
+ this.currentUser = input.user ?? null;
2882
2882
  this.identity = input.user ? createIdentityProvider(input.user.userRef, input.user.getToken) : null;
2883
2883
  this.activePet = input.activePet;
2884
2884
  this.store = failure === null ? createEntitlementStore() : createEntitlementStore(() => deniedDecisionsFor("sdk_config", userFacingError("accessDenied")));
@@ -2912,6 +2912,9 @@ var EverfurRuntimeImpl = class {
2912
2912
  get isDisposed() {
2913
2913
  return this.disposed;
2914
2914
  }
2915
+ get scopeEpoch() {
2916
+ return this.epoch;
2917
+ }
2915
2918
  /**
2916
2919
  * Build the entitlement poller. `authFor` re-derives the device AuthContext each poll (so a token refresh /
2917
2920
  * re-scope is reflected), and `update` feeds the resolved decisions into the render store. The SERVER stays
@@ -1,5 +1,5 @@
1
1
  export { K as KvPort } from '../cacheEpoch-DknKn3S0.cjs';
2
- export { C as CapabilityKind, a as CapabilityRegistry, b as CapabilityServices, c as ChatControllerRegistry, d as ChatScope, e as CreateEverfurRuntimeInput, f as CreateRuntimeOptions, E as EntitlementPoller, g as EntitlementPollerDeps, h as EntitlementSnapshot, i as EntitlementStore, j as EverfurRuntime, k as EverfurRuntimeConfig, L as LogLevel, l as LogRecord, m as LogSink, M as MIN_POLL_INTERVAL_SECONDS, N as NULL_SECURE_STORE, P as PollScheduler, R as RuntimeTheme, n as RuntimeUser, S as SecureStorePort, T as TimerHandle, o as createEntitlementPoller, p as createEverfurRuntime } from '../runtime-BgQnA594.cjs';
2
+ export { C as CapabilityKind, a as CapabilityRegistry, b as CapabilityServices, c as ChatControllerRegistry, d as ChatScope, e as CreateEverfurRuntimeInput, f as CreateRuntimeOptions, E as EntitlementPoller, g as EntitlementPollerDeps, h as EntitlementSnapshot, i as EntitlementStore, j as EverfurRuntime, k as EverfurRuntimeConfig, L as LogLevel, l as LogRecord, m as LogSink, M as MIN_POLL_INTERVAL_SECONDS, N as NULL_SECURE_STORE, P as PollScheduler, R as RuntimeTheme, n as RuntimeUser, S as SecureStorePort, T as TimerHandle, o as createEntitlementPoller, p as createEverfurRuntime } from '../runtime-Bvr8yqXm.cjs';
3
3
  export { T as TelemetryPort, n as noopTelemetry } from '../TelemetryPort-BDNr00hu.cjs';
4
4
  import { S as SuggestedPromptCategory, C as ChatController } from '../ChatController-CpUMvvZf.cjs';
5
5
  export { a as ChatControllerDeps, b as ChatHandlers, c as ChatMessage, d as ChatSnapshot, e as ChatStatus, f as Citation, E as ErrorPolicyPort, M as MESSAGE_MAX_CHARS, g as SendOptions, T as Timers, h as createChatController, i as defaultErrorPolicy } from '../ChatController-CpUMvvZf.cjs';
@@ -1,5 +1,5 @@
1
1
  export { K as KvPort } from '../cacheEpoch-DknKn3S0.js';
2
- export { C as CapabilityKind, a as CapabilityRegistry, b as CapabilityServices, c as ChatControllerRegistry, d as ChatScope, e as CreateEverfurRuntimeInput, f as CreateRuntimeOptions, E as EntitlementPoller, g as EntitlementPollerDeps, h as EntitlementSnapshot, i as EntitlementStore, j as EverfurRuntime, k as EverfurRuntimeConfig, L as LogLevel, l as LogRecord, m as LogSink, M as MIN_POLL_INTERVAL_SECONDS, N as NULL_SECURE_STORE, P as PollScheduler, R as RuntimeTheme, n as RuntimeUser, S as SecureStorePort, T as TimerHandle, o as createEntitlementPoller, p as createEverfurRuntime } from '../runtime-CBA-LvdM.js';
2
+ export { C as CapabilityKind, a as CapabilityRegistry, b as CapabilityServices, c as ChatControllerRegistry, d as ChatScope, e as CreateEverfurRuntimeInput, f as CreateRuntimeOptions, E as EntitlementPoller, g as EntitlementPollerDeps, h as EntitlementSnapshot, i as EntitlementStore, j as EverfurRuntime, k as EverfurRuntimeConfig, L as LogLevel, l as LogRecord, m as LogSink, M as MIN_POLL_INTERVAL_SECONDS, N as NULL_SECURE_STORE, P as PollScheduler, R as RuntimeTheme, n as RuntimeUser, S as SecureStorePort, T as TimerHandle, o as createEntitlementPoller, p as createEverfurRuntime } from '../runtime-BIYzf9cK.js';
3
3
  export { T as TelemetryPort, n as noopTelemetry } from '../TelemetryPort-BDNr00hu.js';
4
4
  import { S as SuggestedPromptCategory, C as ChatController } from '../ChatController-CKdBvPj2.js';
5
5
  export { a as ChatControllerDeps, b as ChatHandlers, c as ChatMessage, d as ChatSnapshot, e as ChatStatus, f as Citation, E as ErrorPolicyPort, M as MESSAGE_MAX_CHARS, g as SendOptions, T as Timers, h as createChatController, i as defaultErrorPolicy } from '../ChatController-CKdBvPj2.js';
@@ -2876,7 +2876,7 @@ var EverfurRuntimeImpl = class {
2876
2876
  this.uploadTransport = cfg.uploadTransport ?? unavailableUploadTransport;
2877
2877
  this.imageCache = cfg.imageCache ?? noopImageCache;
2878
2878
  this.kv = cfg.storage ?? createInMemoryKv();
2879
- this.currentUser = input.user;
2879
+ this.currentUser = input.user ?? null;
2880
2880
  this.identity = input.user ? createIdentityProvider(input.user.userRef, input.user.getToken) : null;
2881
2881
  this.activePet = input.activePet;
2882
2882
  this.store = failure === null ? createEntitlementStore() : createEntitlementStore(() => deniedDecisionsFor("sdk_config", userFacingError("accessDenied")));
@@ -2910,6 +2910,9 @@ var EverfurRuntimeImpl = class {
2910
2910
  get isDisposed() {
2911
2911
  return this.disposed;
2912
2912
  }
2913
+ get scopeEpoch() {
2914
+ return this.epoch;
2915
+ }
2913
2916
  /**
2914
2917
  * Build the entitlement poller. `authFor` re-derives the device AuthContext each poll (so a token refresh /
2915
2918
  * re-scope is reflected), and `update` feeds the resolved decisions into the render store. The SERVER stays
package/dist/index.cjs CHANGED
@@ -3230,7 +3230,7 @@ var EverfurRuntimeImpl = class {
3230
3230
  this.uploadTransport = cfg.uploadTransport ?? unavailableUploadTransport;
3231
3231
  this.imageCache = cfg.imageCache ?? noopImageCache;
3232
3232
  this.kv = cfg.storage ?? createInMemoryKv();
3233
- this.currentUser = input.user;
3233
+ this.currentUser = input.user ?? null;
3234
3234
  this.identity = input.user ? createIdentityProvider(input.user.userRef, input.user.getToken) : null;
3235
3235
  this.activePet = input.activePet;
3236
3236
  this.store = failure === null ? createEntitlementStore() : createEntitlementStore(() => deniedDecisionsFor("sdk_config", userFacingError("accessDenied")));
@@ -3264,6 +3264,9 @@ var EverfurRuntimeImpl = class {
3264
3264
  get isDisposed() {
3265
3265
  return this.disposed;
3266
3266
  }
3267
+ get scopeEpoch() {
3268
+ return this.epoch;
3269
+ }
3267
3270
  /**
3268
3271
  * Build the entitlement poller. `authFor` re-derives the device AuthContext each poll (so a token refresh /
3269
3272
  * re-scope is reflected), and `update` feeds the resolved decisions into the render store. The SERVER stays
@@ -3626,7 +3629,7 @@ function EverfurProvider(props) {
3626
3629
  const configKey = `${config?.publishableKey ?? ""}|${config?.apiBaseUrl ?? ""}|${config?.contractVersion ?? ""}`;
3627
3630
  const runtimeRef = react.useRef(null);
3628
3631
  const supersededRef = react.useRef(null);
3629
- const [, scheduleRender] = react.useReducer((n) => n + 1, 0);
3632
+ const [renderTick, scheduleRender] = react.useReducer((n) => n + 1, 0);
3630
3633
  const imperativeUserRef = react.useRef(null);
3631
3634
  const lastPropUserRef = react.useRef(void 0);
3632
3635
  const propUserRefKey = user?.userRef ?? null;
@@ -3718,9 +3721,12 @@ function EverfurProvider(props) {
3718
3721
  runtime,
3719
3722
  handle,
3720
3723
  userRef: userRefKey,
3721
- activePet: effectivePet
3724
+ activePet: effectivePet,
3725
+ scopeEpoch: runtime.scopeEpoch
3722
3726
  }),
3723
- [runtime, handle, userRefKey, effectivePet]
3727
+ // renderTick is in the deps precisely BECAUSE it is not in the body: an imperative verb bumps it,
3728
+ // which re-reads runtime.scopeEpoch above. Without it the epoch is captured once and frozen.
3729
+ [runtime, handle, userRefKey, effectivePet, renderTick]
3724
3730
  );
3725
3731
  return react.createElement(
3726
3732
  EverfurContext.Provider,
@@ -3854,7 +3860,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
3854
3860
  suggestedPrompts: Object.freeze([])
3855
3861
  });
3856
3862
  function useEverfurChat(opts) {
3857
- const { runtime, userRef: userRef2, activePet } = useEverfurContext();
3863
+ const { runtime, userRef: userRef2, activePet, scopeEpoch } = useEverfurContext();
3858
3864
  const petRef2 = opts?.petRef ?? activePet ?? null;
3859
3865
  const conversationId2 = opts?.conversationId ?? null;
3860
3866
  const [controller, setController] = react.useState(null);
@@ -3865,7 +3871,7 @@ function useEverfurChat(opts) {
3865
3871
  setController((current) => current === acquired ? null : current);
3866
3872
  runtime.registry.releaseChat(acquired);
3867
3873
  };
3868
- }, [runtime, userRef2, petRef2, conversationId2]);
3874
+ }, [runtime, userRef2, petRef2, conversationId2, scopeEpoch]);
3869
3875
  const subscribe = react.useCallback(
3870
3876
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
3871
3877
  [controller]
package/dist/index.d.cts CHANGED
@@ -7,8 +7,8 @@ export { T as TokenProvider } from './identity-Brl-lDd6.cjs';
7
7
  import { b as CapabilityName, C as CapabilityDecision } from './resolve-Dq_4_agU.cjs';
8
8
  export { a as CapabilityDisabled, c as DisabledLevel } from './resolve-Dq_4_agU.cjs';
9
9
  import { K as KvPort, I as ImageCachePort } from './cacheEpoch-DknKn3S0.cjs';
10
- import { S as SecureStorePort, m as LogSink, U as UploadPort, q as UploadTransport, j as EverfurRuntime } from './runtime-BgQnA594.cjs';
11
- export { L as LogLevel, l as LogRecord, r as PresignedPolicy, s as UploadPolicyWire } from './runtime-BgQnA594.cjs';
10
+ import { S as SecureStorePort, m as LogSink, U as UploadPort, q as UploadTransport, j as EverfurRuntime } from './runtime-Bvr8yqXm.cjs';
11
+ export { L as LogLevel, l as LogRecord, r as PresignedPolicy, s as UploadPolicyWire } from './runtime-Bvr8yqXm.cjs';
12
12
  import { T as TelemetryPort } from './TelemetryPort-BDNr00hu.cjs';
13
13
  import { C as CameraPort } from './CameraPort-Cv31Pz7g.cjs';
14
14
  export { a as CaptureOptions, M as MediaSource } from './CameraPort-Cv31Pz7g.cjs';
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export { T as TokenProvider } from './identity-DK9zORrG.js';
7
7
  import { b as CapabilityName, C as CapabilityDecision } from './resolve-Dq_4_agU.js';
8
8
  export { a as CapabilityDisabled, c as DisabledLevel } from './resolve-Dq_4_agU.js';
9
9
  import { K as KvPort, I as ImageCachePort } from './cacheEpoch-DknKn3S0.js';
10
- import { S as SecureStorePort, m as LogSink, U as UploadPort, q as UploadTransport, j as EverfurRuntime } from './runtime-CBA-LvdM.js';
11
- export { L as LogLevel, l as LogRecord, r as PresignedPolicy, s as UploadPolicyWire } from './runtime-CBA-LvdM.js';
10
+ import { S as SecureStorePort, m as LogSink, U as UploadPort, q as UploadTransport, j as EverfurRuntime } from './runtime-BIYzf9cK.js';
11
+ export { L as LogLevel, l as LogRecord, r as PresignedPolicy, s as UploadPolicyWire } from './runtime-BIYzf9cK.js';
12
12
  import { T as TelemetryPort } from './TelemetryPort-BDNr00hu.js';
13
13
  import { C as CameraPort } from './CameraPort-Cv31Pz7g.js';
14
14
  export { a as CaptureOptions, M as MediaSource } from './CameraPort-Cv31Pz7g.js';
package/dist/index.js CHANGED
@@ -3224,7 +3224,7 @@ var EverfurRuntimeImpl = class {
3224
3224
  this.uploadTransport = cfg.uploadTransport ?? unavailableUploadTransport;
3225
3225
  this.imageCache = cfg.imageCache ?? noopImageCache;
3226
3226
  this.kv = cfg.storage ?? createInMemoryKv();
3227
- this.currentUser = input.user;
3227
+ this.currentUser = input.user ?? null;
3228
3228
  this.identity = input.user ? createIdentityProvider(input.user.userRef, input.user.getToken) : null;
3229
3229
  this.activePet = input.activePet;
3230
3230
  this.store = failure === null ? createEntitlementStore() : createEntitlementStore(() => deniedDecisionsFor("sdk_config", userFacingError("accessDenied")));
@@ -3258,6 +3258,9 @@ var EverfurRuntimeImpl = class {
3258
3258
  get isDisposed() {
3259
3259
  return this.disposed;
3260
3260
  }
3261
+ get scopeEpoch() {
3262
+ return this.epoch;
3263
+ }
3261
3264
  /**
3262
3265
  * Build the entitlement poller. `authFor` re-derives the device AuthContext each poll (so a token refresh /
3263
3266
  * re-scope is reflected), and `update` feeds the resolved decisions into the render store. The SERVER stays
@@ -3620,7 +3623,7 @@ function EverfurProvider(props) {
3620
3623
  const configKey = `${config?.publishableKey ?? ""}|${config?.apiBaseUrl ?? ""}|${config?.contractVersion ?? ""}`;
3621
3624
  const runtimeRef = useRef(null);
3622
3625
  const supersededRef = useRef(null);
3623
- const [, scheduleRender] = useReducer((n) => n + 1, 0);
3626
+ const [renderTick, scheduleRender] = useReducer((n) => n + 1, 0);
3624
3627
  const imperativeUserRef = useRef(null);
3625
3628
  const lastPropUserRef = useRef(void 0);
3626
3629
  const propUserRefKey = user?.userRef ?? null;
@@ -3712,9 +3715,12 @@ function EverfurProvider(props) {
3712
3715
  runtime,
3713
3716
  handle,
3714
3717
  userRef: userRefKey,
3715
- activePet: effectivePet
3718
+ activePet: effectivePet,
3719
+ scopeEpoch: runtime.scopeEpoch
3716
3720
  }),
3717
- [runtime, handle, userRefKey, effectivePet]
3721
+ // renderTick is in the deps precisely BECAUSE it is not in the body: an imperative verb bumps it,
3722
+ // which re-reads runtime.scopeEpoch above. Without it the epoch is captured once and frozen.
3723
+ [runtime, handle, userRefKey, effectivePet, renderTick]
3718
3724
  );
3719
3725
  return createElement(
3720
3726
  EverfurContext.Provider,
@@ -3848,7 +3854,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
3848
3854
  suggestedPrompts: Object.freeze([])
3849
3855
  });
3850
3856
  function useEverfurChat(opts) {
3851
- const { runtime, userRef: userRef2, activePet } = useEverfurContext();
3857
+ const { runtime, userRef: userRef2, activePet, scopeEpoch } = useEverfurContext();
3852
3858
  const petRef2 = opts?.petRef ?? activePet ?? null;
3853
3859
  const conversationId2 = opts?.conversationId ?? null;
3854
3860
  const [controller, setController] = useState(null);
@@ -3859,7 +3865,7 @@ function useEverfurChat(opts) {
3859
3865
  setController((current) => current === acquired ? null : current);
3860
3866
  runtime.registry.releaseChat(acquired);
3861
3867
  };
3862
- }, [runtime, userRef2, petRef2, conversationId2]);
3868
+ }, [runtime, userRef2, petRef2, conversationId2, scopeEpoch]);
3863
3869
  const subscribe = useCallback(
3864
3870
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
3865
3871
  [controller]
@@ -1532,7 +1532,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
1532
1532
  var NOOP_UNSUBSCRIBE = () => {
1533
1533
  };
1534
1534
  function usePhotoCheckup(opts) {
1535
- const { runtime, userRef } = useEverfurContext();
1535
+ const { runtime, userRef, scopeEpoch } = useEverfurContext();
1536
1536
  const petRef = opts.petRef;
1537
1537
  const [controller, setController] = react.useState(null);
1538
1538
  react.useEffect(() => {
@@ -1554,7 +1554,7 @@ function usePhotoCheckup(opts) {
1554
1554
  setController((current) => current === acquired ? null : current);
1555
1555
  runtime.registry.release(acquired);
1556
1556
  };
1557
- }, [runtime, userRef, petRef]);
1557
+ }, [runtime, userRef, petRef, scopeEpoch]);
1558
1558
  const subscribe = react.useCallback(
1559
1559
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
1560
1560
  [controller]
@@ -1530,7 +1530,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
1530
1530
  var NOOP_UNSUBSCRIBE = () => {
1531
1531
  };
1532
1532
  function usePhotoCheckup(opts) {
1533
- const { runtime, userRef } = useEverfurContext();
1533
+ const { runtime, userRef, scopeEpoch } = useEverfurContext();
1534
1534
  const petRef = opts.petRef;
1535
1535
  const [controller, setController] = useState(null);
1536
1536
  useEffect(() => {
@@ -1552,7 +1552,7 @@ function usePhotoCheckup(opts) {
1552
1552
  setController((current) => current === acquired ? null : current);
1553
1553
  runtime.registry.release(acquired);
1554
1554
  };
1555
- }, [runtime, userRef, petRef]);
1555
+ }, [runtime, userRef, petRef, scopeEpoch]);
1556
1556
  const subscribe = useCallback(
1557
1557
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
1558
1558
  [controller]
@@ -1561,7 +1561,7 @@ var POLL_INTERVAL_MS = 4e3;
1561
1561
  var MAX_POLLS = 30;
1562
1562
  function useEverfurRecords(opts) {
1563
1563
  const { petRef } = opts;
1564
- const { runtime, userRef } = useEverfurContext();
1564
+ const { runtime, userRef, scopeEpoch } = useEverfurContext();
1565
1565
  const [controller, setController] = react.useState(opts.controller ?? null);
1566
1566
  const [snap, setSnap] = react.useState(INITIAL);
1567
1567
  const generation = react.useRef(0);
@@ -1594,7 +1594,7 @@ function useEverfurRecords(opts) {
1594
1594
  runtime.registry.release(acquired);
1595
1595
  setController((current) => current === acquired ? null : current);
1596
1596
  };
1597
- }, [runtime, userRef, petRef, opts.controller]);
1597
+ }, [runtime, userRef, petRef, opts.controller, scopeEpoch]);
1598
1598
  const patch = react.useCallback((partial) => {
1599
1599
  setSnap((current) => ({ ...current, ...partial }));
1600
1600
  }, []);
@@ -1559,7 +1559,7 @@ var POLL_INTERVAL_MS = 4e3;
1559
1559
  var MAX_POLLS = 30;
1560
1560
  function useEverfurRecords(opts) {
1561
1561
  const { petRef } = opts;
1562
- const { runtime, userRef } = useEverfurContext();
1562
+ const { runtime, userRef, scopeEpoch } = useEverfurContext();
1563
1563
  const [controller, setController] = useState(opts.controller ?? null);
1564
1564
  const [snap, setSnap] = useState(INITIAL);
1565
1565
  const generation = useRef(0);
@@ -1592,7 +1592,7 @@ function useEverfurRecords(opts) {
1592
1592
  runtime.registry.release(acquired);
1593
1593
  setController((current) => current === acquired ? null : current);
1594
1594
  };
1595
- }, [runtime, userRef, petRef, opts.controller]);
1595
+ }, [runtime, userRef, petRef, opts.controller, scopeEpoch]);
1596
1596
  const patch = useCallback((partial) => {
1597
1597
  setSnap((current) => ({ ...current, ...partial }));
1598
1598
  }, []);
@@ -277,6 +277,19 @@ interface EverfurRuntime {
277
277
  * dead runtime.
278
278
  */
279
279
  readonly isDisposed: boolean;
280
+ /**
281
+ * Bumped by every re-scope. React surfaces put this in their acquire-effect deps so an imperative
282
+ * verb that changes no identity STRING still re-acquires.
283
+ *
284
+ * WHY THIS EXISTS. `logout()` on an already-anonymous runtime rotates the session id and re-scopes,
285
+ * but nothing a React effect watches changes: `userRef` is null on both sides, so the context memo
286
+ * republishes the same object and the effect never re-runs. The mounted surface kept serving the
287
+ * PREVIOUS anonymous visitor's scope, because the captured AuthContext holds the OLD session id and
288
+ * the widget plane derives anonymous identity as `anon-{session_id}`. On a shared device that is one
289
+ * visitor reading the last one's conversation. The epoch is the only thing that changes in that
290
+ * transition, so it is the only honest dep.
291
+ */
292
+ readonly scopeEpoch: number;
280
293
  readonly entitlements: EntitlementStore;
281
294
  /** null => logout: drops the cached bearer and re-scopes every open capability. */
282
295
  setUser(user: RuntimeUser | null): void;
@@ -277,6 +277,19 @@ interface EverfurRuntime {
277
277
  * dead runtime.
278
278
  */
279
279
  readonly isDisposed: boolean;
280
+ /**
281
+ * Bumped by every re-scope. React surfaces put this in their acquire-effect deps so an imperative
282
+ * verb that changes no identity STRING still re-acquires.
283
+ *
284
+ * WHY THIS EXISTS. `logout()` on an already-anonymous runtime rotates the session id and re-scopes,
285
+ * but nothing a React effect watches changes: `userRef` is null on both sides, so the context memo
286
+ * republishes the same object and the effect never re-runs. The mounted surface kept serving the
287
+ * PREVIOUS anonymous visitor's scope, because the captured AuthContext holds the OLD session id and
288
+ * the widget plane derives anonymous identity as `anon-{session_id}`. On a shared device that is one
289
+ * visitor reading the last one's conversation. The epoch is the only thing that changes in that
290
+ * transition, so it is the only honest dep.
291
+ */
292
+ readonly scopeEpoch: number;
280
293
  readonly entitlements: EntitlementStore;
281
294
  /** null => logout: drops the cached bearer and re-scopes every open capability. */
282
295
  setUser(user: RuntimeUser | null): void;
@@ -1,4 +1,4 @@
1
- import { h as EntitlementSnapshot } from '../../runtime-BgQnA594.cjs';
1
+ import { h as EntitlementSnapshot } from '../../runtime-Bvr8yqXm.cjs';
2
2
  export { C as ClientBranding } from '../../petsRepository-BEGb97M9.cjs';
3
3
  export { a as CapabilityDisabled, c as DisabledLevel, E as EntitlementDecision } from '../../resolve-Dq_4_agU.cjs';
4
4
  import { ReactNode } from 'react';
@@ -1,4 +1,4 @@
1
- import { h as EntitlementSnapshot } from '../../runtime-CBA-LvdM.js';
1
+ import { h as EntitlementSnapshot } from '../../runtime-BIYzf9cK.js';
2
2
  export { C as ClientBranding } from '../../petsRepository-Bu18r2kK.js';
3
3
  export { a as CapabilityDisabled, c as DisabledLevel, E as EntitlementDecision } from '../../resolve-Dq_4_agU.js';
4
4
  import { ReactNode } from 'react';
@@ -1513,7 +1513,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
1513
1513
  var NOOP_UNSUBSCRIBE = () => {
1514
1514
  };
1515
1515
  function useEverfurVideo(opts) {
1516
- const { runtime, userRef, activePet } = useEverfurContext();
1516
+ const { runtime, userRef, activePet, scopeEpoch } = useEverfurContext();
1517
1517
  const petRef = opts?.petRef ?? activePet ?? null;
1518
1518
  const [controller, setController] = react.useState(null);
1519
1519
  react.useEffect(() => {
@@ -1536,7 +1536,7 @@ function useEverfurVideo(opts) {
1536
1536
  setController((current) => current === acquired ? null : current);
1537
1537
  runtime.registry.release(acquired);
1538
1538
  };
1539
- }, [runtime, userRef, petRef]);
1539
+ }, [runtime, userRef, petRef, scopeEpoch]);
1540
1540
  const subscribe = react.useCallback(
1541
1541
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
1542
1542
  [controller]
@@ -1511,7 +1511,7 @@ var BOOTSTRAP_SNAPSHOT = Object.freeze({
1511
1511
  var NOOP_UNSUBSCRIBE = () => {
1512
1512
  };
1513
1513
  function useEverfurVideo(opts) {
1514
- const { runtime, userRef, activePet } = useEverfurContext();
1514
+ const { runtime, userRef, activePet, scopeEpoch } = useEverfurContext();
1515
1515
  const petRef = opts?.petRef ?? activePet ?? null;
1516
1516
  const [controller, setController] = useState(null);
1517
1517
  useEffect(() => {
@@ -1534,7 +1534,7 @@ function useEverfurVideo(opts) {
1534
1534
  setController((current) => current === acquired ? null : current);
1535
1535
  runtime.registry.release(acquired);
1536
1536
  };
1537
- }, [runtime, userRef, petRef]);
1537
+ }, [runtime, userRef, petRef, scopeEpoch]);
1538
1538
  const subscribe = useCallback(
1539
1539
  (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
1540
1540
  [controller]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everfur/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Embed Everfur veterinary chat, health records, and pet-state surfaces in your React Native app.",
5
5
  "license": "MIT",
6
6
  "repository": {