@oxy.so/core 7.10.1 → 7.10.3
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/dist/cjs/HttpService.js +34 -5
- package/dist/esm/HttpService.js +34 -5
- package/dist/types/HttpService.d.ts +4 -2
- package/package.json +2 -2
- package/src/HttpService.ts +37 -6
package/dist/cjs/HttpService.js
CHANGED
|
@@ -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
|
+
// A legacy bearer may omit deviceId; empty or non-string values also carry no
|
|
62
|
+
// usable device. Two known, different devices are distinct local contexts.
|
|
63
|
+
if (typeof a.deviceId === 'string' &&
|
|
64
|
+
a.deviceId.length > 0 &&
|
|
65
|
+
typeof b.deviceId === 'string' &&
|
|
66
|
+
b.deviceId.length > 0 &&
|
|
67
|
+
a.deviceId !== b.deviceId)
|
|
68
|
+
return false;
|
|
61
69
|
// A refresh may rotate exp/iat/jti while preserving its account and session.
|
|
62
70
|
// Opaque/malformed context fields cannot establish that equivalence.
|
|
63
71
|
return [
|
|
@@ -71,6 +79,13 @@ function sameTokenSession(left, right) {
|
|
|
71
79
|
'authMethodId',
|
|
72
80
|
].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
|
|
73
81
|
}
|
|
82
|
+
/** Extract a known device for local ownership checks, never authorization. */
|
|
83
|
+
function knownTokenDeviceId(token) {
|
|
84
|
+
const claims = (0, tokenClaims_1.decodeTokenClaims)(token);
|
|
85
|
+
return claims?.type !== 'service' && typeof claims?.deviceId === 'string' && claims.deviceId
|
|
86
|
+
? claims.deviceId
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
74
89
|
/** Discard an unused body even when a platform's stream shim lacks cancel. */
|
|
75
90
|
function discardResponseBody(response) {
|
|
76
91
|
try {
|
|
@@ -328,6 +343,8 @@ class HttpService {
|
|
|
328
343
|
* bearer — and through the token listeners, the account — back.
|
|
329
344
|
*/
|
|
330
345
|
this.sessionEpoch = 0;
|
|
346
|
+
/** Preserve a known device across legacy rotations which omit the claim. */
|
|
347
|
+
this.sessionDeviceId = null;
|
|
331
348
|
/**
|
|
332
349
|
* Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
|
|
333
350
|
* native Commons-identity re-mint arm: after a sign-out a 401 is the expected
|
|
@@ -566,8 +583,12 @@ class HttpService {
|
|
|
566
583
|
const authHeader = usesSessionAuth
|
|
567
584
|
? await (0, requestSignal_1.awaitRequestSignal)(this.getAuthHeader(), signal)
|
|
568
585
|
: null;
|
|
569
|
-
const isOriginalSession = () =>
|
|
570
|
-
|
|
586
|
+
const isOriginalSession = () => {
|
|
587
|
+
if (this.sessionEpoch !== contextEpoch)
|
|
588
|
+
return false;
|
|
589
|
+
const currentToken = this.syncAccessTokenFromProvider();
|
|
590
|
+
return this.sessionEpoch === contextEpoch && sameTokenSession(contextToken, currentToken);
|
|
591
|
+
};
|
|
571
592
|
const newerRequestToken = () => {
|
|
572
593
|
if (!contextToken || !authHeader?.startsWith('Bearer '))
|
|
573
594
|
return null;
|
|
@@ -770,6 +791,7 @@ class HttpService {
|
|
|
770
791
|
if (this.tokenStore.getAccessToken())
|
|
771
792
|
this.sessionEpoch += 1;
|
|
772
793
|
this.tokenStore.clearTokens();
|
|
794
|
+
this.sessionDeviceId = null;
|
|
773
795
|
selfClearedEpoch = this.sessionEpoch;
|
|
774
796
|
this.notifyTokenChange();
|
|
775
797
|
}
|
|
@@ -1653,8 +1675,13 @@ class HttpService {
|
|
|
1653
1675
|
// Token management
|
|
1654
1676
|
setTokens(accessToken) {
|
|
1655
1677
|
const current = this.tokenStore.getAccessToken();
|
|
1656
|
-
|
|
1678
|
+
const deviceId = knownTokenDeviceId(accessToken);
|
|
1679
|
+
const changed = current !== null &&
|
|
1680
|
+
(!sameTokenSession(current, accessToken) ||
|
|
1681
|
+
(this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
|
|
1682
|
+
if (changed)
|
|
1657
1683
|
this.sessionEpoch += 1;
|
|
1684
|
+
this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
|
|
1658
1685
|
this.sessionEnded = false;
|
|
1659
1686
|
this.tokenStore.setTokens(accessToken);
|
|
1660
1687
|
this.notifyTokenChange();
|
|
@@ -1673,8 +1700,8 @@ class HttpService {
|
|
|
1673
1700
|
this.clearTokens();
|
|
1674
1701
|
}
|
|
1675
1702
|
/**
|
|
1676
|
-
* The current context epoch: account/session switches
|
|
1677
|
-
* invalidate pending work;
|
|
1703
|
+
* The current context epoch: account/session switches, known device changes,
|
|
1704
|
+
* and bearer removal invalidate pending work; same-context rotation preserves it.
|
|
1678
1705
|
* A re-mint lane that
|
|
1679
1706
|
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
1680
1707
|
*/
|
|
@@ -1705,6 +1732,7 @@ class HttpService {
|
|
|
1705
1732
|
if (this.tokenStore.getAccessToken())
|
|
1706
1733
|
this.sessionEpoch += 1;
|
|
1707
1734
|
this.tokenStore.clearTokens();
|
|
1735
|
+
this.sessionDeviceId = null;
|
|
1708
1736
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
1709
1737
|
// different user could never read these entries, but a logged-out client
|
|
1710
1738
|
// must not keep the previous session's personalized data resident in
|
|
@@ -1864,6 +1892,7 @@ class HttpService {
|
|
|
1864
1892
|
__resetTokensForTests() {
|
|
1865
1893
|
this.sessionEnded = false;
|
|
1866
1894
|
this.tokenStore.clearTokens();
|
|
1895
|
+
this.sessionDeviceId = null;
|
|
1867
1896
|
}
|
|
1868
1897
|
}
|
|
1869
1898
|
exports.HttpService = HttpService;
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -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
|
+
// A legacy bearer may omit deviceId; empty or non-string values also carry no
|
|
59
|
+
// usable device. Two known, different devices are distinct local contexts.
|
|
60
|
+
if (typeof a.deviceId === 'string' &&
|
|
61
|
+
a.deviceId.length > 0 &&
|
|
62
|
+
typeof b.deviceId === 'string' &&
|
|
63
|
+
b.deviceId.length > 0 &&
|
|
64
|
+
a.deviceId !== b.deviceId)
|
|
65
|
+
return false;
|
|
58
66
|
// A refresh may rotate exp/iat/jti while preserving its account and session.
|
|
59
67
|
// Opaque/malformed context fields cannot establish that equivalence.
|
|
60
68
|
return [
|
|
@@ -68,6 +76,13 @@ function sameTokenSession(left, right) {
|
|
|
68
76
|
'authMethodId',
|
|
69
77
|
].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
|
|
70
78
|
}
|
|
79
|
+
/** Extract a known device for local ownership checks, never authorization. */
|
|
80
|
+
function knownTokenDeviceId(token) {
|
|
81
|
+
const claims = decodeTokenClaims(token);
|
|
82
|
+
return claims?.type !== 'service' && typeof claims?.deviceId === 'string' && claims.deviceId
|
|
83
|
+
? claims.deviceId
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
71
86
|
/** Discard an unused body even when a platform's stream shim lacks cancel. */
|
|
72
87
|
function discardResponseBody(response) {
|
|
73
88
|
try {
|
|
@@ -325,6 +340,8 @@ export class HttpService {
|
|
|
325
340
|
* bearer — and through the token listeners, the account — back.
|
|
326
341
|
*/
|
|
327
342
|
this.sessionEpoch = 0;
|
|
343
|
+
/** Preserve a known device across legacy rotations which omit the claim. */
|
|
344
|
+
this.sessionDeviceId = null;
|
|
328
345
|
/**
|
|
329
346
|
* Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
|
|
330
347
|
* native Commons-identity re-mint arm: after a sign-out a 401 is the expected
|
|
@@ -563,8 +580,12 @@ export class HttpService {
|
|
|
563
580
|
const authHeader = usesSessionAuth
|
|
564
581
|
? await awaitRequestSignal(this.getAuthHeader(), signal)
|
|
565
582
|
: null;
|
|
566
|
-
const isOriginalSession = () =>
|
|
567
|
-
|
|
583
|
+
const isOriginalSession = () => {
|
|
584
|
+
if (this.sessionEpoch !== contextEpoch)
|
|
585
|
+
return false;
|
|
586
|
+
const currentToken = this.syncAccessTokenFromProvider();
|
|
587
|
+
return this.sessionEpoch === contextEpoch && sameTokenSession(contextToken, currentToken);
|
|
588
|
+
};
|
|
568
589
|
const newerRequestToken = () => {
|
|
569
590
|
if (!contextToken || !authHeader?.startsWith('Bearer '))
|
|
570
591
|
return null;
|
|
@@ -767,6 +788,7 @@ export class HttpService {
|
|
|
767
788
|
if (this.tokenStore.getAccessToken())
|
|
768
789
|
this.sessionEpoch += 1;
|
|
769
790
|
this.tokenStore.clearTokens();
|
|
791
|
+
this.sessionDeviceId = null;
|
|
770
792
|
selfClearedEpoch = this.sessionEpoch;
|
|
771
793
|
this.notifyTokenChange();
|
|
772
794
|
}
|
|
@@ -1650,8 +1672,13 @@ export class HttpService {
|
|
|
1650
1672
|
// Token management
|
|
1651
1673
|
setTokens(accessToken) {
|
|
1652
1674
|
const current = this.tokenStore.getAccessToken();
|
|
1653
|
-
|
|
1675
|
+
const deviceId = knownTokenDeviceId(accessToken);
|
|
1676
|
+
const changed = current !== null &&
|
|
1677
|
+
(!sameTokenSession(current, accessToken) ||
|
|
1678
|
+
(this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
|
|
1679
|
+
if (changed)
|
|
1654
1680
|
this.sessionEpoch += 1;
|
|
1681
|
+
this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
|
|
1655
1682
|
this.sessionEnded = false;
|
|
1656
1683
|
this.tokenStore.setTokens(accessToken);
|
|
1657
1684
|
this.notifyTokenChange();
|
|
@@ -1670,8 +1697,8 @@ export class HttpService {
|
|
|
1670
1697
|
this.clearTokens();
|
|
1671
1698
|
}
|
|
1672
1699
|
/**
|
|
1673
|
-
* The current context epoch: account/session switches
|
|
1674
|
-
* invalidate pending work;
|
|
1700
|
+
* The current context epoch: account/session switches, known device changes,
|
|
1701
|
+
* and bearer removal invalidate pending work; same-context rotation preserves it.
|
|
1675
1702
|
* A re-mint lane that
|
|
1676
1703
|
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
1677
1704
|
*/
|
|
@@ -1702,6 +1729,7 @@ export class HttpService {
|
|
|
1702
1729
|
if (this.tokenStore.getAccessToken())
|
|
1703
1730
|
this.sessionEpoch += 1;
|
|
1704
1731
|
this.tokenStore.clearTokens();
|
|
1732
|
+
this.sessionDeviceId = null;
|
|
1705
1733
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
1706
1734
|
// different user could never read these entries, but a logged-out client
|
|
1707
1735
|
// must not keep the previous session's personalized data resident in
|
|
@@ -1861,6 +1889,7 @@ export class HttpService {
|
|
|
1861
1889
|
__resetTokensForTests() {
|
|
1862
1890
|
this.sessionEnded = false;
|
|
1863
1891
|
this.tokenStore.clearTokens();
|
|
1892
|
+
this.sessionDeviceId = null;
|
|
1864
1893
|
}
|
|
1865
1894
|
}
|
|
1866
1895
|
/**
|
|
@@ -228,6 +228,8 @@ export declare class HttpService {
|
|
|
228
228
|
* bearer — and through the token listeners, the account — back.
|
|
229
229
|
*/
|
|
230
230
|
private sessionEpoch;
|
|
231
|
+
/** Preserve a known device across legacy rotations which omit the claim. */
|
|
232
|
+
private sessionDeviceId;
|
|
231
233
|
/**
|
|
232
234
|
* Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
|
|
233
235
|
* native Commons-identity re-mint arm: after a sign-out a 401 is the expected
|
|
@@ -509,8 +511,8 @@ export declare class HttpService {
|
|
|
509
511
|
*/
|
|
510
512
|
endSession(): void;
|
|
511
513
|
/**
|
|
512
|
-
* The current context epoch: account/session switches
|
|
513
|
-
* invalidate pending work;
|
|
514
|
+
* The current context epoch: account/session switches, known device changes,
|
|
515
|
+
* and bearer removal invalidate pending work; same-context rotation preserves it.
|
|
514
516
|
* A re-mint lane that
|
|
515
517
|
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
516
518
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "7.10.
|
|
3
|
+
"version": "7.10.3",
|
|
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.
|
|
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",
|
package/src/HttpService.ts
CHANGED
|
@@ -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
|
+
// A legacy bearer may omit deviceId; empty or non-string values also carry no
|
|
78
|
+
// usable device. Two known, different devices are distinct local contexts.
|
|
79
|
+
if (
|
|
80
|
+
typeof a.deviceId === 'string' &&
|
|
81
|
+
a.deviceId.length > 0 &&
|
|
82
|
+
typeof b.deviceId === 'string' &&
|
|
83
|
+
b.deviceId.length > 0 &&
|
|
84
|
+
a.deviceId !== b.deviceId
|
|
85
|
+
)
|
|
86
|
+
return false;
|
|
77
87
|
// A refresh may rotate exp/iat/jti while preserving its account and session.
|
|
78
88
|
// Opaque/malformed context fields cannot establish that equivalence.
|
|
79
89
|
return [
|
|
@@ -88,6 +98,14 @@ function sameTokenSession(left: string | null, right: string | null): boolean {
|
|
|
88
98
|
].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
/** Extract a known device for local ownership checks, never authorization. */
|
|
102
|
+
function knownTokenDeviceId(token: string): string | null {
|
|
103
|
+
const claims = decodeTokenClaims(token);
|
|
104
|
+
return claims?.type !== 'service' && typeof claims?.deviceId === 'string' && claims.deviceId
|
|
105
|
+
? claims.deviceId
|
|
106
|
+
: null;
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
/** Discard an unused body even when a platform's stream shim lacks cancel. */
|
|
92
110
|
function discardResponseBody(response: Response): Promise<void> {
|
|
93
111
|
try {
|
|
@@ -529,6 +547,8 @@ export class HttpService {
|
|
|
529
547
|
* bearer — and through the token listeners, the account — back.
|
|
530
548
|
*/
|
|
531
549
|
private sessionEpoch = 0;
|
|
550
|
+
/** Preserve a known device across legacy rotations which omit the claim. */
|
|
551
|
+
private sessionDeviceId: string | null = null;
|
|
532
552
|
/**
|
|
533
553
|
* Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
|
|
534
554
|
* native Commons-identity re-mint arm: after a sign-out a 401 is the expected
|
|
@@ -825,9 +845,11 @@ export class HttpService {
|
|
|
825
845
|
const authHeader = usesSessionAuth
|
|
826
846
|
? await awaitRequestSignal(this.getAuthHeader(), signal)
|
|
827
847
|
: null;
|
|
828
|
-
const isOriginalSession = () =>
|
|
829
|
-
this.sessionEpoch
|
|
830
|
-
|
|
848
|
+
const isOriginalSession = () => {
|
|
849
|
+
if (this.sessionEpoch !== contextEpoch) return false;
|
|
850
|
+
const currentToken = this.syncAccessTokenFromProvider();
|
|
851
|
+
return this.sessionEpoch === contextEpoch && sameTokenSession(contextToken, currentToken);
|
|
852
|
+
};
|
|
831
853
|
const newerRequestToken = () => {
|
|
832
854
|
if (!contextToken || !authHeader?.startsWith('Bearer ')) return null;
|
|
833
855
|
const requestToken = authHeader.slice('Bearer '.length);
|
|
@@ -1060,6 +1082,7 @@ export class HttpService {
|
|
|
1060
1082
|
if (isOriginalSession()) {
|
|
1061
1083
|
if (this.tokenStore.getAccessToken()) this.sessionEpoch += 1;
|
|
1062
1084
|
this.tokenStore.clearTokens();
|
|
1085
|
+
this.sessionDeviceId = null;
|
|
1063
1086
|
selfClearedEpoch = this.sessionEpoch;
|
|
1064
1087
|
this.notifyTokenChange();
|
|
1065
1088
|
}
|
|
@@ -2080,7 +2103,13 @@ export class HttpService {
|
|
|
2080
2103
|
// Token management
|
|
2081
2104
|
setTokens(accessToken: string): void {
|
|
2082
2105
|
const current = this.tokenStore.getAccessToken();
|
|
2083
|
-
|
|
2106
|
+
const deviceId = knownTokenDeviceId(accessToken);
|
|
2107
|
+
const changed =
|
|
2108
|
+
current !== null &&
|
|
2109
|
+
(!sameTokenSession(current, accessToken) ||
|
|
2110
|
+
(this.sessionDeviceId !== null && deviceId !== null && this.sessionDeviceId !== deviceId));
|
|
2111
|
+
if (changed) this.sessionEpoch += 1;
|
|
2112
|
+
this.sessionDeviceId = !current || changed ? deviceId : (deviceId ?? this.sessionDeviceId);
|
|
2084
2113
|
this.sessionEnded = false;
|
|
2085
2114
|
this.tokenStore.setTokens(accessToken);
|
|
2086
2115
|
this.notifyTokenChange();
|
|
@@ -2100,8 +2129,8 @@ export class HttpService {
|
|
|
2100
2129
|
}
|
|
2101
2130
|
|
|
2102
2131
|
/**
|
|
2103
|
-
* The current context epoch: account/session switches
|
|
2104
|
-
* invalidate pending work;
|
|
2132
|
+
* The current context epoch: account/session switches, known device changes,
|
|
2133
|
+
* and bearer removal invalidate pending work; same-context rotation preserves it.
|
|
2105
2134
|
* A re-mint lane that
|
|
2106
2135
|
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
2107
2136
|
*/
|
|
@@ -2136,6 +2165,7 @@ export class HttpService {
|
|
|
2136
2165
|
clearTokens(): void {
|
|
2137
2166
|
if (this.tokenStore.getAccessToken()) this.sessionEpoch += 1;
|
|
2138
2167
|
this.tokenStore.clearTokens();
|
|
2168
|
+
this.sessionDeviceId = null;
|
|
2139
2169
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
2140
2170
|
// different user could never read these entries, but a logged-out client
|
|
2141
2171
|
// must not keep the previous session's personalized data resident in
|
|
@@ -2309,5 +2339,6 @@ export class HttpService {
|
|
|
2309
2339
|
__resetTokensForTests(): void {
|
|
2310
2340
|
this.sessionEnded = false;
|
|
2311
2341
|
this.tokenStore.clearTokens();
|
|
2342
|
+
this.sessionDeviceId = null;
|
|
2312
2343
|
}
|
|
2313
2344
|
}
|