@oxy.so/core 7.10.2 → 7.10.4

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.
@@ -58,6 +58,14 @@ function sameTokenSession(left, right) {
58
58
  }
59
59
  if (!(a.userId || a.id || a.sub) || !(b.userId || b.id || b.sub))
60
60
  return false;
61
+ const leftBinding = knownTokenContext(left);
62
+ const rightBinding = knownTokenContext(right);
63
+ if ((leftBinding.actor && rightBinding.actor && leftBinding.actor !== rightBinding.actor) ||
64
+ (leftBinding.context && rightBinding.context && leftBinding.context !== rightBinding.context) ||
65
+ (leftBinding.deviceSession &&
66
+ rightBinding.deviceSession &&
67
+ leftBinding.deviceSession !== rightBinding.deviceSession))
68
+ return false;
61
69
  // A legacy bearer may omit deviceId; empty or non-string values also carry no
62
70
  // usable device. Two known, different devices are distinct local contexts.
63
71
  if (typeof a.deviceId === 'string' &&
@@ -79,6 +87,20 @@ function sameTokenSession(left, right) {
79
87
  'authMethodId',
80
88
  ].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
81
89
  }
90
+ /** Decoded claims only fence local cache/transport; they do not authorize a token. */
91
+ function knownTokenContext(token) {
92
+ const claims = (0, tokenClaims_1.decodeTokenClaims)(token);
93
+ if (!claims || claims.type === 'service')
94
+ return { actor: null, context: null, deviceSession: null };
95
+ const actor = claims.act;
96
+ const actorId = actor && typeof actor === 'object' ? actor.sub : undefined;
97
+ const known = (value) => (typeof value === 'string' && value.length > 0 ? value : null);
98
+ return {
99
+ actor: known(actorId),
100
+ context: known(claims.device_context_id),
101
+ deviceSession: known(claims.device_session_id),
102
+ };
103
+ }
82
104
  /** Extract a known device for local ownership checks, never authorization. */
83
105
  function knownTokenDeviceId(token) {
84
106
  const claims = (0, tokenClaims_1.decodeTokenClaims)(token);
@@ -345,6 +367,10 @@ class HttpService {
345
367
  this.sessionEpoch = 0;
346
368
  /** Preserve a known device across legacy rotations which omit the claim. */
347
369
  this.sessionDeviceId = null;
370
+ /** Preserve known actor and context hints across rotations without those claims. */
371
+ this.sessionActorId = null;
372
+ this.sessionContextId = null;
373
+ this.sessionDeviceSessionId = null;
348
374
  /**
349
375
  * Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
350
376
  * native Commons-identity re-mint arm: after a sign-out a 401 is the expected
@@ -792,6 +818,9 @@ class HttpService {
792
818
  this.sessionEpoch += 1;
793
819
  this.tokenStore.clearTokens();
794
820
  this.sessionDeviceId = null;
821
+ this.sessionActorId = null;
822
+ this.sessionContextId = null;
823
+ this.sessionDeviceSessionId = null;
795
824
  selfClearedEpoch = this.sessionEpoch;
796
825
  this.notifyTokenChange();
797
826
  }
@@ -1676,12 +1705,30 @@ class HttpService {
1676
1705
  setTokens(accessToken) {
1677
1706
  const current = this.tokenStore.getAccessToken();
1678
1707
  const deviceId = knownTokenDeviceId(accessToken);
1708
+ const binding = knownTokenContext(accessToken);
1679
1709
  const changed = current !== null &&
1680
1710
  (!sameTokenSession(current, accessToken) ||
1681
- (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
1711
+ (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId) ||
1712
+ (this.sessionActorId !== null &&
1713
+ binding.actor !== null &&
1714
+ this.sessionActorId !== binding.actor) ||
1715
+ (this.sessionContextId !== null &&
1716
+ binding.context !== null &&
1717
+ this.sessionContextId !== binding.context) ||
1718
+ (this.sessionDeviceSessionId !== null &&
1719
+ binding.deviceSession !== null &&
1720
+ this.sessionDeviceSessionId !== binding.deviceSession));
1682
1721
  if (changed)
1683
1722
  this.sessionEpoch += 1;
1684
1723
  this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
1724
+ this.sessionActorId =
1725
+ !current || changed ? binding.actor : (binding.actor ?? this.sessionActorId);
1726
+ this.sessionContextId =
1727
+ !current || changed ? binding.context : (binding.context ?? this.sessionContextId);
1728
+ this.sessionDeviceSessionId =
1729
+ !current || changed
1730
+ ? binding.deviceSession
1731
+ : (binding.deviceSession ?? this.sessionDeviceSessionId);
1685
1732
  this.sessionEnded = false;
1686
1733
  this.tokenStore.setTokens(accessToken);
1687
1734
  this.notifyTokenChange();
@@ -1733,6 +1780,9 @@ class HttpService {
1733
1780
  this.sessionEpoch += 1;
1734
1781
  this.tokenStore.clearTokens();
1735
1782
  this.sessionDeviceId = null;
1783
+ this.sessionActorId = null;
1784
+ this.sessionContextId = null;
1785
+ this.sessionDeviceSessionId = null;
1736
1786
  // Drop the response cache on logout. The cache is identity-scoped, so a
1737
1787
  // different user could never read these entries, but a logged-out client
1738
1788
  // must not keep the previous session's personalized data resident in
@@ -1893,6 +1943,9 @@ class HttpService {
1893
1943
  this.sessionEnded = false;
1894
1944
  this.tokenStore.clearTokens();
1895
1945
  this.sessionDeviceId = null;
1946
+ this.sessionActorId = null;
1947
+ this.sessionContextId = null;
1948
+ this.sessionDeviceSessionId = null;
1896
1949
  }
1897
1950
  }
1898
1951
  exports.HttpService = HttpService;
@@ -55,6 +55,14 @@ function sameTokenSession(left, right) {
55
55
  }
56
56
  if (!(a.userId || a.id || a.sub) || !(b.userId || b.id || b.sub))
57
57
  return false;
58
+ const leftBinding = knownTokenContext(left);
59
+ const rightBinding = knownTokenContext(right);
60
+ if ((leftBinding.actor && rightBinding.actor && leftBinding.actor !== rightBinding.actor) ||
61
+ (leftBinding.context && rightBinding.context && leftBinding.context !== rightBinding.context) ||
62
+ (leftBinding.deviceSession &&
63
+ rightBinding.deviceSession &&
64
+ leftBinding.deviceSession !== rightBinding.deviceSession))
65
+ return false;
58
66
  // A legacy bearer may omit deviceId; empty or non-string values also carry no
59
67
  // usable device. Two known, different devices are distinct local contexts.
60
68
  if (typeof a.deviceId === 'string' &&
@@ -76,6 +84,20 @@ function sameTokenSession(left, right) {
76
84
  'authMethodId',
77
85
  ].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
78
86
  }
87
+ /** Decoded claims only fence local cache/transport; they do not authorize a token. */
88
+ function knownTokenContext(token) {
89
+ const claims = decodeTokenClaims(token);
90
+ if (!claims || claims.type === 'service')
91
+ return { actor: null, context: null, deviceSession: null };
92
+ const actor = claims.act;
93
+ const actorId = actor && typeof actor === 'object' ? actor.sub : undefined;
94
+ const known = (value) => (typeof value === 'string' && value.length > 0 ? value : null);
95
+ return {
96
+ actor: known(actorId),
97
+ context: known(claims.device_context_id),
98
+ deviceSession: known(claims.device_session_id),
99
+ };
100
+ }
79
101
  /** Extract a known device for local ownership checks, never authorization. */
80
102
  function knownTokenDeviceId(token) {
81
103
  const claims = decodeTokenClaims(token);
@@ -342,6 +364,10 @@ export class HttpService {
342
364
  this.sessionEpoch = 0;
343
365
  /** Preserve a known device across legacy rotations which omit the claim. */
344
366
  this.sessionDeviceId = null;
367
+ /** Preserve known actor and context hints across rotations without those claims. */
368
+ this.sessionActorId = null;
369
+ this.sessionContextId = null;
370
+ this.sessionDeviceSessionId = null;
345
371
  /**
346
372
  * Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
347
373
  * native Commons-identity re-mint arm: after a sign-out a 401 is the expected
@@ -789,6 +815,9 @@ export class HttpService {
789
815
  this.sessionEpoch += 1;
790
816
  this.tokenStore.clearTokens();
791
817
  this.sessionDeviceId = null;
818
+ this.sessionActorId = null;
819
+ this.sessionContextId = null;
820
+ this.sessionDeviceSessionId = null;
792
821
  selfClearedEpoch = this.sessionEpoch;
793
822
  this.notifyTokenChange();
794
823
  }
@@ -1673,12 +1702,30 @@ export class HttpService {
1673
1702
  setTokens(accessToken) {
1674
1703
  const current = this.tokenStore.getAccessToken();
1675
1704
  const deviceId = knownTokenDeviceId(accessToken);
1705
+ const binding = knownTokenContext(accessToken);
1676
1706
  const changed = current !== null &&
1677
1707
  (!sameTokenSession(current, accessToken) ||
1678
- (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
1708
+ (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId) ||
1709
+ (this.sessionActorId !== null &&
1710
+ binding.actor !== null &&
1711
+ this.sessionActorId !== binding.actor) ||
1712
+ (this.sessionContextId !== null &&
1713
+ binding.context !== null &&
1714
+ this.sessionContextId !== binding.context) ||
1715
+ (this.sessionDeviceSessionId !== null &&
1716
+ binding.deviceSession !== null &&
1717
+ this.sessionDeviceSessionId !== binding.deviceSession));
1679
1718
  if (changed)
1680
1719
  this.sessionEpoch += 1;
1681
1720
  this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
1721
+ this.sessionActorId =
1722
+ !current || changed ? binding.actor : (binding.actor ?? this.sessionActorId);
1723
+ this.sessionContextId =
1724
+ !current || changed ? binding.context : (binding.context ?? this.sessionContextId);
1725
+ this.sessionDeviceSessionId =
1726
+ !current || changed
1727
+ ? binding.deviceSession
1728
+ : (binding.deviceSession ?? this.sessionDeviceSessionId);
1682
1729
  this.sessionEnded = false;
1683
1730
  this.tokenStore.setTokens(accessToken);
1684
1731
  this.notifyTokenChange();
@@ -1730,6 +1777,9 @@ export class HttpService {
1730
1777
  this.sessionEpoch += 1;
1731
1778
  this.tokenStore.clearTokens();
1732
1779
  this.sessionDeviceId = null;
1780
+ this.sessionActorId = null;
1781
+ this.sessionContextId = null;
1782
+ this.sessionDeviceSessionId = null;
1733
1783
  // Drop the response cache on logout. The cache is identity-scoped, so a
1734
1784
  // different user could never read these entries, but a logged-out client
1735
1785
  // must not keep the previous session's personalized data resident in
@@ -1890,6 +1940,9 @@ export class HttpService {
1890
1940
  this.sessionEnded = false;
1891
1941
  this.tokenStore.clearTokens();
1892
1942
  this.sessionDeviceId = null;
1943
+ this.sessionActorId = null;
1944
+ this.sessionContextId = null;
1945
+ this.sessionDeviceSessionId = null;
1893
1946
  }
1894
1947
  }
1895
1948
  /**
@@ -230,6 +230,10 @@ export declare class HttpService {
230
230
  private sessionEpoch;
231
231
  /** Preserve a known device across legacy rotations which omit the claim. */
232
232
  private sessionDeviceId;
233
+ /** Preserve known actor and context hints across rotations without those claims. */
234
+ private sessionActorId;
235
+ private sessionContextId;
236
+ private sessionDeviceSessionId;
233
237
  /**
234
238
  * Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
235
239
  * native Commons-identity re-mint arm: after a sign-out a 401 is the expected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "7.10.2",
3
+ "version": "7.10.4",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -181,7 +181,7 @@
181
181
  "dependencies": {
182
182
  "@noble/ciphers": "^1.3.0",
183
183
  "@noble/hashes": "^1.8.0",
184
- "@oxy.so/contracts": "6.1.0",
184
+ "@oxy.so/contracts": "6.2.0",
185
185
  "@oxy.so/protocol": "^1.5.1",
186
186
  "@oxy.so/telemetry": "^2.1.0",
187
187
  "@scure/bip39": "^1.6.0",
@@ -74,6 +74,16 @@ function sameTokenSession(left: string | null, right: string | null): boolean {
74
74
  );
75
75
  }
76
76
  if (!(a.userId || a.id || a.sub) || !(b.userId || b.id || b.sub)) return false;
77
+ const leftBinding = knownTokenContext(left);
78
+ const rightBinding = knownTokenContext(right);
79
+ if (
80
+ (leftBinding.actor && rightBinding.actor && leftBinding.actor !== rightBinding.actor) ||
81
+ (leftBinding.context && rightBinding.context && leftBinding.context !== rightBinding.context) ||
82
+ (leftBinding.deviceSession &&
83
+ rightBinding.deviceSession &&
84
+ leftBinding.deviceSession !== rightBinding.deviceSession)
85
+ )
86
+ return false;
77
87
  // A legacy bearer may omit deviceId; empty or non-string values also carry no
78
88
  // usable device. Two known, different devices are distinct local contexts.
79
89
  if (
@@ -98,6 +108,25 @@ function sameTokenSession(left: string | null, right: string | null): boolean {
98
108
  ].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
99
109
  }
100
110
 
111
+ /** Decoded claims only fence local cache/transport; they do not authorize a token. */
112
+ function knownTokenContext(token: string): {
113
+ actor: string | null;
114
+ context: string | null;
115
+ deviceSession: string | null;
116
+ } {
117
+ const claims = decodeTokenClaims(token);
118
+ if (!claims || claims.type === 'service')
119
+ return { actor: null, context: null, deviceSession: null };
120
+ const actor = claims.act;
121
+ const actorId = actor && typeof actor === 'object' ? (actor as { sub?: unknown }).sub : undefined;
122
+ const known = (value: unknown) => (typeof value === 'string' && value.length > 0 ? value : null);
123
+ return {
124
+ actor: known(actorId),
125
+ context: known(claims.device_context_id),
126
+ deviceSession: known(claims.device_session_id),
127
+ };
128
+ }
129
+
101
130
  /** Extract a known device for local ownership checks, never authorization. */
102
131
  function knownTokenDeviceId(token: string): string | null {
103
132
  const claims = decodeTokenClaims(token);
@@ -549,6 +578,10 @@ export class HttpService {
549
578
  private sessionEpoch = 0;
550
579
  /** Preserve a known device across legacy rotations which omit the claim. */
551
580
  private sessionDeviceId: string | null = null;
581
+ /** Preserve known actor and context hints across rotations without those claims. */
582
+ private sessionActorId: string | null = null;
583
+ private sessionContextId: string | null = null;
584
+ private sessionDeviceSessionId: string | null = null;
552
585
  /**
553
586
  * Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
554
587
  * native Commons-identity re-mint arm: after a sign-out a 401 is the expected
@@ -1083,6 +1116,9 @@ export class HttpService {
1083
1116
  if (this.tokenStore.getAccessToken()) this.sessionEpoch += 1;
1084
1117
  this.tokenStore.clearTokens();
1085
1118
  this.sessionDeviceId = null;
1119
+ this.sessionActorId = null;
1120
+ this.sessionContextId = null;
1121
+ this.sessionDeviceSessionId = null;
1086
1122
  selfClearedEpoch = this.sessionEpoch;
1087
1123
  this.notifyTokenChange();
1088
1124
  }
@@ -2104,12 +2140,30 @@ export class HttpService {
2104
2140
  setTokens(accessToken: string): void {
2105
2141
  const current = this.tokenStore.getAccessToken();
2106
2142
  const deviceId = knownTokenDeviceId(accessToken);
2143
+ const binding = knownTokenContext(accessToken);
2107
2144
  const changed =
2108
2145
  current !== null &&
2109
2146
  (!sameTokenSession(current, accessToken) ||
2110
- (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
2147
+ (this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId) ||
2148
+ (this.sessionActorId !== null &&
2149
+ binding.actor !== null &&
2150
+ this.sessionActorId !== binding.actor) ||
2151
+ (this.sessionContextId !== null &&
2152
+ binding.context !== null &&
2153
+ this.sessionContextId !== binding.context) ||
2154
+ (this.sessionDeviceSessionId !== null &&
2155
+ binding.deviceSession !== null &&
2156
+ this.sessionDeviceSessionId !== binding.deviceSession));
2111
2157
  if (changed) this.sessionEpoch += 1;
2112
2158
  this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
2159
+ this.sessionActorId =
2160
+ !current || changed ? binding.actor : (binding.actor ?? this.sessionActorId);
2161
+ this.sessionContextId =
2162
+ !current || changed ? binding.context : (binding.context ?? this.sessionContextId);
2163
+ this.sessionDeviceSessionId =
2164
+ !current || changed
2165
+ ? binding.deviceSession
2166
+ : (binding.deviceSession ?? this.sessionDeviceSessionId);
2113
2167
  this.sessionEnded = false;
2114
2168
  this.tokenStore.setTokens(accessToken);
2115
2169
  this.notifyTokenChange();
@@ -2166,6 +2220,9 @@ export class HttpService {
2166
2220
  if (this.tokenStore.getAccessToken()) this.sessionEpoch += 1;
2167
2221
  this.tokenStore.clearTokens();
2168
2222
  this.sessionDeviceId = null;
2223
+ this.sessionActorId = null;
2224
+ this.sessionContextId = null;
2225
+ this.sessionDeviceSessionId = null;
2169
2226
  // Drop the response cache on logout. The cache is identity-scoped, so a
2170
2227
  // different user could never read these entries, but a logged-out client
2171
2228
  // must not keep the previous session's personalized data resident in
@@ -2340,5 +2397,8 @@ export class HttpService {
2340
2397
  this.sessionEnded = false;
2341
2398
  this.tokenStore.clearTokens();
2342
2399
  this.sessionDeviceId = null;
2400
+ this.sessionActorId = null;
2401
+ this.sessionContextId = null;
2402
+ this.sessionDeviceSessionId = null;
2343
2403
  }
2344
2404
  }